Fixed issues with UDP connection handling. Fixed issues with

registration order. Fixed issues with connection shutdown. Fixed
issues with explicit flush(). Added support for UDP session handling.
Updated broadcast discovery to return ports used. Added
connection.send(), which will use the best option to send
 data (TCP first, otherwise UDP).
This commit is contained in:
nathan 2018-04-01 14:51:13 +02:00
parent ab174e4e0e
commit d9ca5b93df
72 changed files with 2090 additions and 1079 deletions

View File

@ -29,6 +29,8 @@ import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import dorkbox.network.pipeline.MagicBytes;
import dorkbox.network.pipeline.discovery.BroadcastResponse;
import dorkbox.network.pipeline.discovery.ClientDiscoverHostHandler;
import dorkbox.network.pipeline.discovery.ClientDiscoverHostInitializer;
import dorkbox.util.OS;
@ -48,9 +50,6 @@ import io.netty.channel.socket.oio.OioDatagramChannel;
@SuppressWarnings({"unused", "AutoBoxing"})
public final
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(Client.class.getSimpleName());
/**
@ -74,10 +73,10 @@ class Broadcast {
* @return the first server found, or null if no server responded.
*/
public static
String discoverHost(int udpPort, int discoverTimeoutMillis) {
InetAddress discoverHost = discoverHostAddress(udpPort, discoverTimeoutMillis);
BroadcastResponse discoverHost(int udpPort, int discoverTimeoutMillis) {
BroadcastResponse discoverHost = discoverHostAddress(udpPort, discoverTimeoutMillis);
if (discoverHost != null) {
return discoverHost.getHostAddress();
return discoverHost;
}
return null;
}
@ -93,8 +92,8 @@ class Broadcast {
* @return the first server found, or null if no server responded.
*/
public static
InetAddress discoverHostAddress(int udpPort, int discoverTimeoutMillis) {
List<InetAddress> servers = discoverHost0(udpPort, discoverTimeoutMillis, false);
BroadcastResponse discoverHostAddress(int udpPort, int discoverTimeoutMillis) {
List<BroadcastResponse> servers = discoverHost0(udpPort, discoverTimeoutMillis, false);
if (servers.isEmpty()) {
return null;
}
@ -114,18 +113,18 @@ class Broadcast {
* @return the list of found servers (if they responded)
*/
public static
List<InetAddress> discoverHosts(int udpPort, int discoverTimeoutMillis) {
List<BroadcastResponse> discoverHosts(int udpPort, int discoverTimeoutMillis) {
return discoverHost0(udpPort, discoverTimeoutMillis, true);
}
private static
List<InetAddress> discoverHost0(int udpPort, int discoverTimeoutMillis, boolean fetchAllServers) {
List<BroadcastResponse> discoverHost0(int udpPort, int discoverTimeoutMillis, boolean fetchAllServers) {
// fetch a buffer that contains the serialized object.
ByteBuf buffer = Unpooled.buffer(1);
buffer.writeByte(broadcastID);
buffer.writeByte(MagicBytes.broadcastID);
List<InetAddress> servers = new ArrayList<InetAddress>();
List<BroadcastResponse> servers = new ArrayList<BroadcastResponse>();
Logger logger2 = logger;
@ -134,7 +133,7 @@ class Broadcast {
networkInterfaces = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e) {
logger2.error("Host discovery failed.", e);
return new ArrayList<InetAddress>(0);
return new ArrayList<BroadcastResponse>(0);
}
@ -155,7 +154,7 @@ class Broadcast {
try {
if (logger2.isInfoEnabled()) {
logger2.info("Searching for host on {}:{}", address, udpPort);
logger2.info("Searching for host on [{}:{}]", address.getHostAddress(), udpPort);
}
EventLoopGroup group;
@ -207,9 +206,8 @@ class Broadcast {
}
}
else {
InetSocketAddress attachment = channel1.attr(ClientDiscoverHostHandler.STATE)
.get();
servers.add(attachment.getAddress());
BroadcastResponse broadcastResponse = channel1.attr(ClientDiscoverHostHandler.STATE).get();
servers.add(broadcastResponse);
}
@ -244,9 +242,9 @@ class Broadcast {
}
}
else {
InetSocketAddress attachment = channel1.attr(ClientDiscoverHostHandler.STATE)
.get();
servers.add(attachment.getAddress());
BroadcastResponse broadcastResponse = channel1.attr(ClientDiscoverHostHandler.STATE).get();
servers.add(broadcastResponse);
if (!fetchAllServers) {
break;
}
@ -272,21 +270,48 @@ class Broadcast {
if (logger2.isInfoEnabled() && !servers.isEmpty()) {
StringBuilder stringBuilder = new StringBuilder(256);
if (fetchAllServers) {
StringBuilder stringBuilder = new StringBuilder(256);
stringBuilder.append("Discovered servers: (")
.append(servers.size())
.append(")");
for (InetAddress server : servers) {
for (BroadcastResponse server : servers) {
stringBuilder.append("/n")
.append(server)
.append(":")
.append(udpPort);
.append(server.remoteAddress)
.append(":");
if (server.tcpPort > 0) {
stringBuilder.append(server.tcpPort);
if (server.udpPort > 0) {
stringBuilder.append(":");
}
}
if (server.udpPort > 0) {
stringBuilder.append(udpPort);
}
}
logger2.info(stringBuilder.toString());
}
else {
logger2.info("Discovered server: {}:{}", servers.get(0), udpPort);
BroadcastResponse server = servers.get(0);
stringBuilder.append(server.remoteAddress)
.append(":");
if (server.tcpPort > 0) {
stringBuilder.append(server.tcpPort);
if (server.udpPort > 0) {
stringBuilder.append(":");
}
}
if (server.udpPort > 0) {
stringBuilder.append(udpPort);
}
logger2.info("Discovered server [{}]", stringBuilder.toString());
}
}

View File

@ -15,6 +15,8 @@
*/
package dorkbox.network;
import static dorkbox.network.pipeline.ConnectionType.LOCAL;
import java.io.IOException;
import java.net.InetSocketAddress;
@ -30,26 +32,20 @@ import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerC
import dorkbox.network.rmi.RemoteObject;
import dorkbox.network.rmi.RemoteObjectCallback;
import dorkbox.network.rmi.TimeoutException;
import dorkbox.util.NamedThreadFactory;
import dorkbox.util.OS;
import dorkbox.util.exceptions.SecurityException;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelOption;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.kqueue.KQueueDatagramChannel;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueueSocketChannel;
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;
@ -112,41 +108,18 @@ class Client<C extends Connection> extends EndPointClient implements Connection
localChannelName = config.localChannelName;
hostName = config.host;
final EventLoopGroup workerEventLoop = newEventLoop(DEFAULT_THREAD_POOL_SIZE, threadName);
final EventLoopGroup boss;
if (OS.isAndroid()) {
// android ONLY supports OIO (not NIO)
boss = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
boss = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// KQueue network stack is MUCH faster (but only on macosx)
boss = new KQueueEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
else {
boss = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
manageForShutdown(boss);
if (config.localChannelName != null && config.tcpPort <= 0 && config.udpPort <= 0) {
// no networked bootstraps. LOCAL connection only
Bootstrap localBootstrap = new Bootstrap();
bootstraps.add(new BootstrapWrapper("LOCAL", config.localChannelName, -1, localBootstrap));
EventLoopGroup localBoss = new DefaultEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-LOCAL",
threadGroup));
localBootstrap.group(localBoss)
localBootstrap.group(newEventLoop(LOCAL, 1, threadName + "-JVM-BOSS"))
.channel(LocalChannel.class)
.remoteAddress(new LocalAddress(config.localChannelName))
.handler(new RegistrationLocalHandlerClient(threadName, registrationWrapper));
manageForShutdown(localBoss);
.handler(new RegistrationLocalHandlerClient(threadName, registrationWrapper, workerEventLoop));
}
else {
if (config.host == null) {
@ -166,7 +139,7 @@ class Client<C extends Connection> extends EndPointClient implements Connection
tcpBootstrap.channel(OioSocketChannel.class);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
// epoll network stack is MUCH faster (but only on linux)
tcpBootstrap.channel(EpollSocketChannel.class);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
@ -177,12 +150,11 @@ class Client<C extends Connection> extends EndPointClient implements Connection
tcpBootstrap.channel(NioSocketChannel.class);
}
tcpBootstrap.group(boss)
tcpBootstrap.group(newEventLoop(1, threadName + "-TCP-BOSS"))
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
.remoteAddress(config.host, config.tcpPort)
.handler(new RegistrationRemoteHandlerClientTCP(threadName,
registrationWrapper));
.handler(new RegistrationRemoteHandlerClientTCP(threadName, registrationWrapper, workerEventLoop));
// android screws up on this!!
tcpBootstrap.option(ChannelOption.TCP_NODELAY, !OS.isAndroid())
@ -199,7 +171,7 @@ class Client<C extends Connection> extends EndPointClient implements Connection
udpBootstrap.channel(OioDatagramChannel.class);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
// epoll network stack is MUCH faster (but only on linux)
udpBootstrap.channel(EpollDatagramChannel.class);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
@ -211,15 +183,14 @@ class Client<C extends Connection> extends EndPointClient implements Connection
}
udpBootstrap.group(boss)
udpBootstrap.group(newEventLoop(1, threadName + "-UDP-BOSS"))
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
// Netty4 has a default of 2048 bytes as upper limit for datagram packets.
.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(EndPoint.udpMaxSize))
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
.localAddress(new InetSocketAddress(0)) // bind to wildcard
.remoteAddress(new InetSocketAddress(config.host, config.udpPort))
.handler(new RegistrationRemoteHandlerClientUDP(threadName,
registrationWrapper));
.handler(new RegistrationRemoteHandlerClientUDP(threadName, registrationWrapper, workerEventLoop));
// Enable to READ and WRITE MULTICAST data (ie, 192.168.1.0)
// in order to WRITE: write as normal, just make sure it ends in .255
@ -253,8 +224,8 @@ class Client<C extends Connection> extends EndPointClient implements Connection
*/
public
void reconnect(final int connectionTimeout) throws IOException {
// close out all old connections
closeConnections();
// make sure we are closed first
close();
connect(connectionTimeout);
}
@ -283,7 +254,7 @@ class Client<C extends Connection> extends EndPointClient implements Connection
* if the client is unable to connect in the requested time
*/
public
void connect(int connectionTimeout) throws IOException {
void connect(final int connectionTimeout) throws IOException {
this.connectionTimeout = connectionTimeout;
// make sure we are not trying to connect during a close or stop event.
@ -291,6 +262,25 @@ class Client<C extends Connection> extends EndPointClient implements Connection
synchronized (shutdownInProgress) {
}
// if we are in the SAME thread as netty -- start in a new thread (otherwise we will deadlock)
if (isNettyThread()) {
runNewThread("Restart Thread", new Runnable(){
@Override
public
void run() {
try {
connect(connectionTimeout);
} catch (IOException e) {
e.printStackTrace();
}
}
});
return;
}
if (isShutdown()) {
throw new IOException("Unable to connect when shutdown...");
}
@ -300,14 +290,25 @@ class Client<C extends Connection> extends EndPointClient implements Connection
}
else {
if (config.tcpPort > 0 && config.udpPort > 0) {
logger.info("Connecting to server: {} at TCP/UDP port: {}", hostName, config.tcpPort, config.udpPort);
} else {
logger.info("Connecting to server: {} at TCP port: {}", hostName, config.tcpPort);
logger.info("Connecting to TCP/UDP server [{}:{}]", hostName, config.tcpPort, config.udpPort);
}
else if (config.tcpPort > 0) {
logger.info("Connecting to TCP server [{}:{}]", hostName, config.tcpPort);
}
else {
logger.info("Connecting to UDP server [{}:{}]", hostName, config.udpPort);
}
}
// have to start the registration process. This will wait until registration is complete and RMI methods are initialized
// if this is called in the event dispatch thread for netty, it will deadlock!
startRegistration();
if (config.tcpPort == 0 && config.udpPort > 0) {
// AFTER registration is complete, if we are UDP only -- setup a heartbeat (must be the larger of 2x the idle timeout OR 10 seconds)
startUdpHeartbeat();
}
}
@Override
@ -471,14 +472,33 @@ class Client<C extends Connection> extends EndPointClient implements Connection
}
/**
* Closes all connections ONLY (keeps the client running). To STOP the client, use stop().
* Closes all connections ONLY (keeps the client running), does not remove any listeners. To STOP the client, use stop().
* <p/>
* This is used, for example, when reconnecting to a server.
*/
@Override
public
void close() {
closeConnections();
closeConnection();
// String threadName = Client.class.getSimpleName();
// synchronized (bootstraps) {
// ArrayList<BootstrapWrapper> newList = new ArrayList<BootstrapWrapper>(bootstraps.size());
//
// for (BootstrapWrapper bootstrap : bootstraps) {
// EventLoopGroup group = bootstrap.bootstrap.group();
//
// removeFromShutdown(group);
// group.shutdownGracefully();
//
// String name = threadName + "-" + bootstrap.type + "-BOSS";
//
// newList.add(bootstrap.clone(newEventLoop(1, name)));
// }
//
// bootstraps.clear();
// bootstraps.addAll(newList);
// }
}
}

View File

@ -245,7 +245,7 @@ class DnsClient extends Shutdownable {
channelType = OioDatagramChannel.class;
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
// epoll network stack is MUCH faster (but only on linux)
eventLoopGroup = new EpollEventLoopGroup(1, new NamedThreadFactory(THREAD_NAME + "-DNS", threadGroup));
channelType = EpollDatagramChannel.class;
}
@ -661,8 +661,8 @@ class DnsClient extends Shutdownable {
}
}
String msg = "Could not ask question to DNS server for A/AAAA record: {}";
logger.error(msg, hostname);
String msg = "Could not ask question to DNS server for A/AAAA record: " + hostname;
logger.error(msg);
UnknownHostException cause = (UnknownHostException) resolve.cause();
if (cause != null) {

View File

@ -23,6 +23,7 @@ import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Shutdownable;
import dorkbox.network.dns.DnsQuestion;
import dorkbox.network.dns.Name;
import dorkbox.network.dns.constants.DnsClass;
import dorkbox.network.dns.constants.DnsRecordType;
import dorkbox.network.dns.records.ARecord;
import dorkbox.network.dns.serverHandlers.DnsServerHandler;
@ -84,7 +85,10 @@ class DnsServer extends Shutdownable {
void main(String[] args) {
DnsServer server = new DnsServer("localhost", 2053);
// server.aRecord("google.com", DnsClass.IN, 10, "127.0.0.1");
// MasterZone zone = new MasterZone();
server.aRecord("google.com", DnsClass.IN, 10, "127.0.0.1");
// server.bind(false);
server.bind();
@ -123,32 +127,31 @@ class DnsServer extends Shutdownable {
final EventLoopGroup boss;
final EventLoopGroup worker;
final EventLoopGroup work;
if (OS.isAndroid()) {
// android ONLY supports OIO (not NIO)
boss = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
worker = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
boss = new OioEventLoopGroup(1, new NamedThreadFactory(threadName + "-boss", threadGroup));
work = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
boss = new EpollEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
worker = new EpollEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
// epoll network stack is MUCH faster (but only on linux)
boss = new EpollEventLoopGroup(1, new NamedThreadFactory(threadName + "-boss", threadGroup));
work = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// KQueue network stack is MUCH faster (but only on macosx)
boss = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
worker = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
boss = new KQueueEventLoopGroup(1, new NamedThreadFactory(threadName + "-boss", threadGroup));
work = new KQueueEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
else {
// sometimes the native libraries cannot be loaded, so fall back to NIO
boss = new NioEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
worker = new NioEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
boss = new NioEventLoopGroup(1, new NamedThreadFactory(threadName + "-boss", threadGroup));
work = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
manageForShutdown(boss);
manageForShutdown(worker);
manageForShutdown(work);
tcpBootstrap = new ServerBootstrap();
@ -160,7 +163,7 @@ class DnsServer extends Shutdownable {
tcpBootstrap.channel(OioServerSocketChannel.class);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
// epoll network stack is MUCH faster (but only on linux)
tcpBootstrap.channel(EpollServerSocketChannel.class);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
@ -174,7 +177,7 @@ class DnsServer extends Shutdownable {
// TODO: If we use netty for an HTTP server,
// Beside the usual ChannelOptions the Native Transport allows to enable TCP_CORK which may come in handy if you implement a HTTP Server.
tcpBootstrap.group(boss, worker)
tcpBootstrap.group(boss, work)
.option(ChannelOption.SO_BACKLOG, backlogConnectionCount)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.SO_KEEPALIVE, true)
@ -200,18 +203,18 @@ class DnsServer extends Shutdownable {
udpBootstrap.channel(OioDatagramChannel.class);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
// epoll network stack is MUCH faster (but only on linux)
udpBootstrap.channel(EpollDatagramChannel.class);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on macosx)
// KQueue network stack is MUCH faster (but only on macosx)
udpBootstrap.channel(KQueueDatagramChannel.class);
}
else {
udpBootstrap.channel(NioDatagramChannel.class);
}
udpBootstrap.group(worker)
udpBootstrap.group(work)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(EndPoint.WRITE_BUFF_LOW, EndPoint.WRITE_BUFF_HIGH))

View File

@ -15,6 +15,8 @@
*/
package dorkbox.network;
import static dorkbox.network.pipeline.ConnectionType.LOCAL;
import java.io.IOException;
import java.net.Socket;
@ -24,7 +26,6 @@ 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.util.NamedThreadFactory;
import dorkbox.util.OS;
import dorkbox.util.Property;
import dorkbox.util.exceptions.SecurityException;
@ -33,20 +34,15 @@ import io.netty.bootstrap.SessionBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.kqueue.KQueueDatagramChannel;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueueServerSocketChannel;
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;
@ -142,7 +138,8 @@ class Server<C extends Connection> extends EndPointServer {
if (udpPort > 0) {
// This is what allows us to have UDP behave "similar" to TCP, in that a session is established based on the port/ip of the
// remote connection. This allows us to reuse channels and have "state" for a UDP connection that normally wouldn't exist.
udpBootstrap = new SessionBootstrap();
// Additionally, this is what responds to discovery broadcast packets
udpBootstrap = new SessionBootstrap(tcpPort, udpPort);
}
else {
udpBootstrap = null;
@ -150,64 +147,34 @@ class Server<C extends Connection> extends EndPointServer {
String threadName = Server.class.getSimpleName();
final EventLoopGroup workerEventLoop = newEventLoop(DEFAULT_THREAD_POOL_SIZE, threadName);
final EventLoopGroup boss;
final EventLoopGroup worker;
if (OS.isAndroid()) {
// android ONLY supports OIO (not NIO)
boss = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
worker = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
boss = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
worker = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// KQueue network stack is MUCH faster (but only on macosx)
boss = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
worker = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
else {
boss = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
worker = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
}
manageForShutdown(boss);
manageForShutdown(worker);
// always use local channels on the server.
{
EventLoopGroup localBoss;
EventLoopGroup localWorker;
if (localBootstrap != null) {
localBoss = new DefaultEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss-LOCAL",
threadGroup));
localWorker = new DefaultEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-worker-LOCAL",
threadGroup));
localBootstrap.group(localBoss, localWorker)
.channel(LocalServerChannel.class)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
.localAddress(new LocalAddress(localChannelName))
.childHandler(new RegistrationLocalHandlerServer(threadName, registrationWrapper));
manageForShutdown(localBoss);
manageForShutdown(localWorker);
}
if (localBootstrap != null) {
localBootstrap.group(newEventLoop(LOCAL, 1, threadName + "-JVM-BOSS"),
newEventLoop(LOCAL, 1, threadName + "-JVM-HAND"))
.channel(LocalServerChannel.class)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
.localAddress(new LocalAddress(localChannelName))
.childHandler(new RegistrationLocalHandlerServer(threadName, registrationWrapper, workerEventLoop));
}
// don't even bother with TCP/UDP if it's not enabled
if (tcpBootstrap == null && udpBootstrap == null) {
return;
}
if (tcpBootstrap != null) {
if (OS.isAndroid()) {
// android ONLY supports OIO (not NIO)
tcpBootstrap.channel(OioServerSocketChannel.class);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
// epoll network stack is MUCH faster (but only on linux)
tcpBootstrap.channel(EpollServerSocketChannel.class);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
@ -221,15 +188,15 @@ class Server<C extends Connection> extends EndPointServer {
// TODO: If we use netty for an HTTP server,
// Beside the usual ChannelOptions the Native Transport allows to enable TCP_CORK which may come in handy if you implement a HTTP Server.
tcpBootstrap.group(boss, worker)
tcpBootstrap.group(newEventLoop(1, threadName + "-TCP-BOSS"),
newEventLoop(1, threadName + "-TCP-HAND"))
.option(ChannelOption.SO_BACKLOG, backlogConnectionCount)
.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new RegistrationRemoteHandlerServerTCP(threadName,
registrationWrapper));
.childHandler(new RegistrationRemoteHandlerServerTCP(threadName, registrationWrapper, workerEventLoop));
// have to check options.host for "0.0.0.0". we don't bind to "0.0.0.0", we bind to "null" to get the "any" address!
if (hostName.equals("0.0.0.0")) {
@ -252,7 +219,7 @@ class Server<C extends Connection> extends EndPointServer {
udpBootstrap.channel(OioDatagramChannel.class);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// JNI network stack is MUCH faster (but only on linux)
// epoll network stack is MUCH faster (but only on linux)
udpBootstrap.channel(EpollDatagramChannel.class);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
@ -267,7 +234,9 @@ class Server<C extends Connection> extends EndPointServer {
// Netty4 has a default of 2048 bytes as upper limit for datagram packets, we want this to be whatever we specify
FixedRecvByteBufAllocator recvByteBufAllocator = new FixedRecvByteBufAllocator(EndPoint.udpMaxSize);
udpBootstrap.group(worker)
udpBootstrap.group(newEventLoop(1, threadName + "-UDP-BOSS"),
newEventLoop(1, threadName + "-UDP-HAND"))
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.option(ChannelOption.RCVBUF_ALLOCATOR, recvByteBufAllocator)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
@ -276,7 +245,7 @@ class Server<C extends Connection> extends EndPointServer {
// TODO: move broadcast to it's own handler, and have UDP server be able to be bound to a specific IP
// OF NOTE: At the end in my case I decided to bind to .255 broadcast address on Linux systems. (to receive broadcast packets)
.localAddress(udpPort) // if you bind to a specific interface, Linux will be unable to receive broadcast packets! see: http://developerweb.net/viewtopic.php?id=5722
.childHandler(new RegistrationRemoteHandlerServerUDP(threadName, registrationWrapper));
.childHandler(new RegistrationRemoteHandlerServerUDP(threadName, registrationWrapper, workerEventLoop));
// // have to check options.host for null. we don't bind to 0.0.0.0, we bind to "null" to get the "any" address!
// if (hostName.equals("0.0.0.0")) {
@ -344,7 +313,7 @@ class Server<C extends Connection> extends EndPointServer {
throw new IllegalArgumentException("Could not bind to LOCAL address '" + localChannelName + "' on the server.", future.cause());
}
logger.info("Listening on LOCAL address: '{}'", localChannelName);
logger.info("Listening on LOCAL address: [{}]", localChannelName);
manageForShutdown(future);
}
@ -365,7 +334,7 @@ class Server<C extends Connection> extends EndPointServer {
throw new IllegalArgumentException("Could not bind to address " + hostName + " TCP port " + tcpPort + " on the server.", future.cause());
}
logger.info("Listening on TCP at {}:{}", hostName, tcpPort);
logger.info("TCP server listen address [{}:{}]", hostName, tcpPort);
manageForShutdown(future);
}
@ -384,7 +353,7 @@ class Server<C extends Connection> extends EndPointServer {
future.cause());
}
logger.info("Listening on UDP at {}:{}", hostName, udpPort);
logger.info("UDP server listen address [{}:{}]", hostName, udpPort);
manageForShutdown(future);
}

View File

@ -16,6 +16,7 @@
package dorkbox.network.connection;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.EventLoopGroup;
public
class BootstrapWrapper {
@ -32,6 +33,11 @@ class BootstrapWrapper {
this.bootstrap = bootstrap;
}
public
BootstrapWrapper clone(EventLoopGroup group) {
return new BootstrapWrapper(type, address, port, bootstrap.clone(group));
}
@Override
public
String toString() {

View File

@ -67,6 +67,12 @@ interface Connection {
*/
ConnectionBridge send();
/**
* Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol
* is available to use. If you want specify the protocol, use {@link #send()}, followed by the protocol you wish to use.
*/
ConnectionPoint send(Object message);
/**
* Expose methods to send objects to a destination when the connection has become idle.
*/
@ -83,7 +89,7 @@ interface Connection {
Listeners listeners();
/**
* Closes the connection
* Closes the connection, but does not remove any listeners
*/
void close();

View File

@ -23,12 +23,16 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.slf4j.Logger;
import dorkbox.network.Client;
import dorkbox.network.connection.Listener.OnMessageReceived;
import dorkbox.network.connection.bridge.ConnectionBridge;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleSender;
@ -54,6 +58,7 @@ import dorkbox.network.serialization.CryptoSerializationManager;
import dorkbox.util.collections.LockFreeHashMap;
import dorkbox.util.collections.LockFreeIntMap;
import dorkbox.util.generics.ClassHelper;
import io.netty.bootstrap.DatagramCloseMessage;
import io.netty.bootstrap.DatagramSessionChannel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable;
@ -80,9 +85,9 @@ import io.netty.util.concurrent.Promise;
@SuppressWarnings("unused")
@Sharable
public
class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConnection, Connection, Listeners, ConnectionBridge {
class ConnectionImpl extends ChannelInboundHandlerAdapter implements CryptoConnection, Connection, Listeners, ConnectionBridge {
public static
boolean isTcp(Class<? extends Channel> channelClass) {
boolean isTcpChannel(Class<? extends Channel> channelClass) {
return channelClass == OioSocketChannel.class ||
channelClass == NioSocketChannel.class ||
channelClass == KQueueSocketChannel.class ||
@ -90,7 +95,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
}
public static
boolean isUdp(Class<? extends Channel> channelClass) {
boolean isUdpChannel(Class<? extends Channel> channelClass) {
return channelClass == OioDatagramChannel.class ||
channelClass == NioDatagramChannel.class ||
channelClass == KQueueDatagramChannel.class ||
@ -99,7 +104,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
}
public static
boolean isLocal(Class<? extends Channel> channelClass) {
boolean isLocalChannel(Class<? extends Channel> channelClass) {
return channelClass == LocalChannel.class;
}
@ -111,15 +116,13 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
private final Object writeLock = new Object();
private final AtomicBoolean closeInProgress = new AtomicBoolean(false);
private final AtomicBoolean alreadyClosed = new AtomicBoolean(false);
private final Object closeInProgressLock = new Object();
private final AtomicBoolean channelIsClosed = new AtomicBoolean(false);
private final Object messageInProgressLock = new Object();
private final AtomicBoolean messageInProgress = new AtomicBoolean(false);
private ISessionManager sessionManager;
private ChannelWrapper channelWrapper;
private boolean isLoopback;
private volatile PingFuture pingFuture = null;
@ -140,6 +143,11 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
// counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
private final AtomicLong aes_gcm_iv = new AtomicLong(0);
// when closing this connection, HOW MANY endpoints need to be closed?
private CountDownLatch closeLatch;
//
// RMI fields
//
@ -150,6 +158,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
private final LockFreeIntMap<RemoteObjectCallback> rmiRegistrationCallbacks;
private volatile int rmiCallbackId = 0;
/**
* All of the parameters can be null, when metaChannel wants to get the base class type
*/
@ -175,8 +184,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
/**
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
* <p/>
* This happens BEFORE prep.
*/
final
void init(final ChannelWrapper channelWrapper, final ISessionManager sessionManager) {
@ -186,23 +193,33 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
//noinspection SimplifiableIfStatement
if (this.channelWrapper instanceof ChannelNetworkWrapper) {
this.remoteKeyChanged = ((ChannelNetworkWrapper) this.channelWrapper).remoteKeyChanged();
int count = 0;
if (channelWrapper.tcp() != null) {
count++;
}
if (channelWrapper.udp() != null) {
count++;
// we received a hint to close this channel from the remote end.
add(new OnMessageReceived<Connection, DatagramCloseMessage>() {
@Override
public
void received(final Connection connection, final DatagramCloseMessage message) {
connection.close();
}
});
}
// when closing this connection, HOW MANY endpoints need to be closed?
closeLatch = new CountDownLatch(count);
}
else {
this.remoteKeyChanged = false;
}
isLoopback = channelWrapper.isLoopback();
}
/**
* Prepare the channel wrapper, since it doesn't have access to certain fields during it's initialization.
* <p/>
* This happens AFTER init.
*/
final
void prep() {
if (this.channelWrapper != null) {
this.channelWrapper.init();
// when closing this connection, HOW MANY endpoints need to be closed?
closeLatch = new CountDownLatch(1);
}
}
@ -256,7 +273,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
@Override
public
boolean isLoopback() {
return isLoopback;
return channelWrapper.isLoopback();
}
/**
@ -330,20 +347,20 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
/**
* INTERNAL USE ONLY. Used to initiate a ping, and to return a ping.
* Sends a ping message attempted in the following order: UDP, TCP
*
* Sends a ping message attempted in the following order: UDP, TCP,LOCAL
*/
public final
void ping0(PingMessage ping) {
if (this.channelWrapper.udp() != null) {
UDP(ping);
UDP(ping).flush();
}
else if (this.channelWrapper.tcp() != null) {
TCP(ping);
TCP(ping).flush();
}
else {
self(ping);
}
flush();
}
/**
@ -417,14 +434,43 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
return this;
}
/**
* Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol
* is available to use. If you want specify the protocol, use {@link #send()}, followed by the protocol you wish to use.
*
* By default, this will try in the following order:
* - TCP (if available)
* - UDP (if available)
* - LOCAL
*/
@Override
public final
ConnectionPoint send(final Object message) {
if (this.channelWrapper.tcp() != null) {
return TCP(message);
}
else if (this.channelWrapper.udp() != null) {
return UDP(message);
}
else {
self(message);
// we have to return something, otherwise dependent code will throw a null pointer exception
return ChannelNull.get();
}
}
/**
* 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) {
ConnectionPoint self(Object message) {
logger.trace("Sending LOCAL {}", message);
this.sessionManager.onMessage(this, message);
// THIS IS REALLY A LOCAL CONNECTION!
return this.channelWrapper.tcp();
}
/**
@ -432,7 +478,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
*/
@Override
public final
ConnectionPoint TCP(Object message) {
ConnectionPoint TCP(final Object message) {
if (!closeInProgress.get()) {
logger.trace("Sending TCP {}", message);
@ -440,7 +486,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
try {
tcp.write(message);
} catch (Exception e) {
logger.error("Unable to write TCP object {}", message.getClass());
logger.error("Unable to write TCP object {}", message.getClass(), e);
}
return tcp;
}
@ -465,7 +511,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
try {
udp.write(message);
} catch (Exception e) {
logger.error("Unable to write TCP object {}", message.getClass());
logger.error("Unable to write UDP object {}", message.getClass(), e);
}
return udp;
}
@ -479,8 +525,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
/**
* Flushes the contents of the TCP/UDP/etc pipes to the actual transport.
*/
@Override
public final
final
void flush() {
this.channelWrapper.flush();
}
@ -532,7 +577,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
ReferenceCountUtil.release(message);
}
public
private
void channelRead(Object object) {
// prevent close from occurring SMACK in the middle of a message in progress.
// delay close until it's finished.
@ -573,8 +618,9 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
Channel channel = context.channel();
Class<? extends Channel> channelClass = channel.getClass();
boolean isTCP = isTcp(channelClass);
boolean isLocal = isLocal(channelClass);
boolean isTCP = isTcpChannel(channelClass);
boolean isUDP = false;
boolean isLocal = isLocalChannel(channelClass);
if (this.logger.isInfoEnabled()) {
String type;
@ -582,55 +628,115 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
if (isTCP) {
type = "TCP";
}
else if (isUdp(channelClass)) {
type = "UDP";
}
else if (isLocal) {
type = "LOCAL";
}
else {
type = "UNKNOWN";
isUDP = isUdpChannel(channelClass);
if (isUDP) {
type = "UDP";
}
else if (isLocal) {
type = "LOCAL";
}
else {
type = "UNKNOWN";
}
}
this.logger.info("Closed remote {} connection: {}",
this.logger.info("Closed remote {} connection [{}]",
type,
channel.remoteAddress()
.toString());
EndPoint.getHostDetails(channel.remoteAddress()));
}
// TODO: tell the remote endpoint that it needs to close (via a message, which might get there...).
if (this.endPoint instanceof EndPointClient) {
((EndPointClient) this.endPoint).abortRegistration();
}
// our master channels are TCP/LOCAL (which are mutually exclusive). Only key disconnect events based on the status of them.
if (isTCP || isLocal) {
// this is because channelInactive can ONLY happen when netty shuts down the channel.
// and connection.close() can be called by the user.
// will auto-flush if necessary
this.sessionManager.onDisconnected(this);
// close TCP/UDP together!
close();
/*
* Only close if we are:
* - local (mutually exclusive to TCP/UDP)
* - TCP (and TCP+UDP)
* - UDP (and not part of TCP+UDP)
*
* DO NOT call close if we are:
* - UDP (part of TCP+UDP)
*/
if (isLocal ||
isTCP ||
(isUDP && this.channelWrapper.tcp() == null)) {
// we can get to this point in two ways. We only want this to happen once
// - remote endpoint disconnects (and so closes us)
// - local endpoint calls close(), and netty will call this.
// this must happen first, because client.close() depends on it!
// onDisconnected() must happen last.
boolean doClose = channelIsClosed.compareAndSet(false, true);
if (!closeInProgress.get()) {
if (endPoint instanceof EndPointClient) {
// client closes single connection
((Client) endPoint).close();
} else {
// server only closes this connection.
close();
}
}
if (doClose) {
// this is because channelInactive can ONLY happen when netty shuts down the channel.
// and connection.close() can be called by the user.
// will auto-flush if necessary
this.sessionManager.onDisconnected(this);
}
}
synchronized (this.closeInProgressLock) {
this.alreadyClosed.set(true);
this.closeInProgressLock.notify();
}
closeLatch.countDown();
// UDP connections ALWAYS have to shutdown their event loop (because of how session management works)
// if (isUDP || this.endPoint instanceof EndPointClient) {
// // also have to shutdown this eventloop, but ONLY for the client!
// channel.eventLoop()
// .shutdownGracefully();
// }
}
/**
* Closes the connection
* Closes the connection, but does not remove any listeners
*/
@Override
public final
void close() {
close(false);
close(true);
}
/**
* we can get to this point in two ways. We only want this to happen once
* - remote endpoint disconnects (and so netty calls us)
* - local endpoint calls close() directly
*
* NOTE: If we remove all listeners and we are the client, then we remove ALL logic from the client!
*/
final
void close(final boolean keepListeners) {
// if we are in the same thread as netty, run in a new thread to prevent deadlocks with messageInProgress
if (!this.closeInProgress.get() && this.messageInProgress.get() && Shutdownable.isNettyThread()) {
Shutdownable.runNewThread("Close connection Thread", new Runnable() {
@Override
public
void run() {
close(keepListeners);
}
});
return;
}
// only close if we aren't already in the middle of closing.
if (this.closeInProgress.compareAndSet(false, true)) {
int idleTimeoutMs = this.endPoint.getIdleTimeout();
@ -641,7 +747,8 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
// if we are in the middle of a message, hold off.
synchronized (this.messageInProgressLock) {
if (this.messageInProgress.get()) {
// while loop is to prevent spurious wakeups!
while (this.messageInProgress.get()) {
try {
this.messageInProgressLock.wait(idleTimeoutMs);
} catch (InterruptedException ignored) {
@ -652,8 +759,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
// flush any pending messages
this.channelWrapper.flush();
this.channelWrapper.close(this, this.sessionManager);
// close out the ping future
PingFuture pingFuture2 = this.pingFuture;
if (pingFuture2 != null) {
@ -661,18 +766,22 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
}
this.pingFuture = null;
// want to wait for the "channelInactive" method to FINISH before allowing our current thread to continue!
synchronized (this.closeInProgressLock) {
if (!this.alreadyClosed.get()) {
synchronized (this.channelIsClosed) {
if (!this.channelIsClosed.get()) {
// this will have netty call "channelInactive()"
this.channelWrapper.close(this, this.sessionManager);
// want to wait for the "channelInactive()" method to FINISH ALL TYPES before allowing our current thread to continue!
try {
this.closeInProgressLock.wait(idleTimeoutMs);
} catch (Exception ignored) {
closeLatch.await(idleTimeoutMs, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
}
}
}
// remove all listeners, but ONLY if we are the server. If we remove all listeners and we are the client, then we remove
// ALL logic from the client! The server is OK because the server listeners per connection are dynamically added
// remove all listeners AFTER we close the channel.
if (!keepListeners) {
removeAll();
}
@ -688,7 +797,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
}
}
/**
* Marks the connection to be closed as soon as possible. This is evaluated when the current
* thread execution returns to the network stack.
@ -958,8 +1066,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
// have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here.
// this means we are creating a NEW object on the server, bound access to only this connection
TCP(message);
flush();
send(message).flush();
}
@Override
@ -985,8 +1092,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
// have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here.
// this means we are creating a NEW object on the server, bound access to only this connection
TCP(message);
flush();
send(message).flush();
}

View File

@ -15,6 +15,7 @@
*/
package dorkbox.network.connection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
@ -201,6 +202,12 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
throw new IllegalArgumentException("listener cannot be null.");
}
if (logger.isTraceEnabled()) {
logger.trace("listener removed: {}",
listener.getClass()
.getName());
}
boolean found = false;
int remainingListeners = 0;
@ -237,12 +244,6 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
if (remainingListeners == 0) {
hasAtLeastOneListener.set(false);
}
if (logger.isTraceEnabled()) {
logger.trace("listener removed: {}",
listener.getClass()
.getName());
}
}
else {
logger.error("No matching listener types. Unable to remove listener: {}",
@ -261,12 +262,12 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
@Override
public final
Listeners removeAll() {
onMessageReceivedManager.removeAll();
onConnectedManager.clear();
onDisconnectedManager.clear();
onIdleManager.clear();
onMessageReceivedManager.clear();
Logger logger2 = this.logger;
if (logger2.isTraceEnabled()) {
logger2.trace("ALL listeners removed !!");
}
logger.trace("ALL listeners removed !!");
return this;
}
@ -311,6 +312,7 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
@Override
public final
void onMessage(final ConnectionImpl connection, final Object message) {
logger.trace("onMessage({}, {})", connection.id(), message.getClass());
notifyOnMessage0(connection, message, false);
}
@ -320,6 +322,9 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
if (connection.manageRmi(message)) {
// if we are an RMI message/registration, we have very specific, defined behavior. We do not use the "normal" listener callback pattern
// because these methods are rare, and require special functionality
// make sure we flush the message to the socket!
connection.flush();
return true;
}
@ -372,13 +377,16 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
}
}
/**
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
*/
@Override
public
void onConnected(final ConnectionImpl connection) {
addConnection(connection);
logger.trace("onConnected({})", connection.id());
// we add the connection in a different step!
boolean foundListener = onConnectedManager.notifyConnected((C) connection, shutdown);
@ -401,7 +409,9 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
@Override
public
void onDisconnected(final ConnectionImpl connection) {
boolean foundListener = onDisconnectedManager.notifyDisconnected((C) connection, shutdown);
logger.trace("onDisconnected({})", connection.id());
boolean foundListener = onDisconnectedManager.notifyDisconnected((C) connection);
if (foundListener) {
connection.flush();
@ -422,6 +432,25 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
removeConnection(connection);
}
/**
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
*/
@Override
public
void addConnection(ConnectionImpl connection) {
logger.trace("addConnection({})", connection.id());
addConnection0(connection);
// now have to account for additional (local) listener managers.
// access a snapshot of the managers (single-writer-principle)
final IdentityMap<Connection, ConnectionManager> localManagers = localManagersREF.get(this);
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
localManager.addConnection(connection);
}
}
/**
* Adds a custom connection to the server.
* <p>
@ -430,7 +459,7 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
*
* @param connection the connection to add
*/
void addConnection(final Connection connection) {
void addConnection0(final Connection connection) {
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
@ -457,7 +486,6 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
*
* @param connection the connection to remove
*/
public
void removeConnection(Connection connection) {
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
@ -599,6 +627,8 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
*/
final
void closeConnections(boolean keepListeners) {
LinkedList<ConnectionImpl> closeConnections = new LinkedList<ConnectionImpl>();
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
@ -609,19 +639,19 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
// Close the connection. Make sure the close operation ends because
// all I/O operations are asynchronous in Netty.
// Also necessary otherwise workers won't close.
if (keepListeners && connection instanceof ConnectionImpl) {
((ConnectionImpl) connection).close(true);
}
else {
connection.close();
if (connection instanceof ConnectionImpl) {
closeConnections.add((ConnectionImpl) connection);
}
}
this.connectionEntries.clear();
this.connectionsHead = null;
}
// must be outside of the synchronize, otherwise we can potentially deadlock
for (ConnectionImpl connection : closeConnections) {
connection.close(keepListeners);
}
}
/**
@ -704,7 +734,7 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
*/
@Override
public
void self(final Object message) {
ConnectionPoint self(final Object message) {
ConcurrentEntry<ConnectionImpl> current = connectionsREF.get(this);
ConnectionImpl c;
while (current != null) {
@ -713,6 +743,7 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
onMessage(c, message);
}
return this;
}
/**
@ -751,6 +782,44 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
return this;
}
/**
* Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol
* is available to use. If you want specify the protocol, use {@link ConnectionManager#TCP(Object)}, etc.
* <p>
* By default, this will try in the following order:
* - TCP (if available)
* - UDP (if available)
* - LOCAL
*/
protected
ConnectionPoint send(final Object message) {
ConcurrentEntry<Connection> current = connectionsREF.get(this);
Connection c;
while (current != null) {
c = current.getValue();
current = current.next();
c.send(message);
}
return this;
}
/**
* Flushes the contents of the TCP/UDP/etc pipes to the actual transport socket.
*/
@Override
public
void flush() {
ConcurrentEntry<ConnectionImpl> current = connectionsREF.get(this);
ConnectionImpl c;
while (current != null) {
c = current.getValue();
current = current.next();
c.flush();
}
}
@Override
public
boolean equals(final Object o) {

View File

@ -30,6 +30,11 @@ interface ConnectionPoint {
*/
void write(Object object) throws Exception;
/**
* Flushes the contents of the TCP/UDP/etc pipes to the actual transport socket.
*/
void flush();
/**
* Creates a new promise associated with this connection type
*/

View File

@ -21,7 +21,7 @@ import org.bouncycastle.crypto.params.ParametersWithIV;
* Supporting methods for encrypting data to a remote endpoint
*/
public
interface ICryptoConnection extends IRmiConnection {
interface CryptoConnection extends RmiConnection, Connection {
/**
* This is the per-message sequence number.

View File

@ -15,7 +15,9 @@
*/
package dorkbox.network.connection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.SecureRandom;
import java.util.List;
import java.util.concurrent.Executor;
@ -44,6 +46,7 @@ import dorkbox.util.Property;
import dorkbox.util.crypto.CryptoECC;
import dorkbox.util.entropy.Entropy;
import dorkbox.util.exceptions.SecurityException;
import io.netty.channel.local.LocalAddress;
import io.netty.util.NetUtil;
/**
@ -63,11 +66,45 @@ class EndPoint extends Shutdownable {
// TODO: maybe some sort of STUN-like connection keep-alive??
public static
String getHostDetails(final SocketAddress socketAddress) {
StringBuilder builder = new StringBuilder();
getHostDetails(builder, socketAddress);
return builder.toString();
}
public static
void getHostDetails(StringBuilder stringBuilder, final SocketAddress socketAddress) {
if (socketAddress instanceof InetSocketAddress) {
InetSocketAddress address = (InetSocketAddress) socketAddress;
InetAddress address1 = address.getAddress();
String hostName = address1.getHostName();
String hostAddress = address1.getHostAddress();
if (!hostName.equals(hostAddress)) {
stringBuilder.append(hostName)
.append('/')
.append(hostAddress);
}
else {
stringBuilder.append(hostAddress);
}
stringBuilder.append(':')
.append(address.getPort());
}
else if (socketAddress instanceof LocalAddress) {
stringBuilder.append(socketAddress.toString());
}
}
/**
* Defines if we are allowed to use the native OS-specific network interface (non-native to java) for boosted networking performance.
*/
@Property
public static boolean enableNativeLibrary = true;
public static boolean enableNativeLibrary = false;
public static final String LOCAL_CHANNEL = "local_channel";
@ -122,7 +159,8 @@ class EndPoint extends Shutdownable {
*/
private volatile int idleTimeoutMs = 0;
private AtomicBoolean isConnected = new AtomicBoolean(false);
// the connection status of this endpoint. Once a server has connected to ANY client, it will always return true until server.close() is called
protected final AtomicBoolean isConnected = new AtomicBoolean(false);
/**
@ -244,7 +282,7 @@ class EndPoint extends Shutdownable {
*/
public
void disableRemoteKeyValidation() {
if (isConnected()) {
if (isConnected.get()) {
logger.error("Cannot disable the remote key validation after this endpoint is connected!");
}
else {
@ -301,16 +339,6 @@ class EndPoint extends Shutdownable {
this.idleTimeoutMs = idleTimeoutMs;
}
/**
* Return the connection status of this endpoint.
* <p/>
* Once a server has connected to ANY client, it will always return true until server.close() is called
*/
public final
boolean isConnected() {
return isConnected.get();
}
/**
* Returns the serialization wrapper if there is an object type that needs to be added outside of the basics.
*/
@ -342,7 +370,7 @@ class EndPoint extends Shutdownable {
* @param remoteAddress be NULL (when getting the baseClass or when creating a local channel)
*/
protected final
Connection connection0(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
ConnectionImpl connection0(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
ConnectionImpl connection;
RmiBridge rmiBridge = null;
@ -357,7 +385,6 @@ class EndPoint extends Shutdownable {
ChannelWrapper wrapper;
connection = newConnection(logger, this, rmiBridge);
metaChannel.connection = connection;
if (metaChannel.localChannel != null) {
if (rmiEnabled) {
@ -378,6 +405,9 @@ class EndPoint extends Shutdownable {
// now initialize the connection channels with whatever extra info they might need.
connection.init(wrapper, connectionManager);
isConnected.set(true);
connectionManager.addConnection(connection);
}
else {
// getting the connection baseClass
@ -393,14 +423,9 @@ class EndPoint extends Shutdownable {
* Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications
* to the pipeline are finished.
* <p/>
* Only the CLIENT injects in front of this)
* Only the CLIENT injects in front of this
*/
void connectionConnected0(ConnectionImpl connection) {
isConnected.set(true);
// prep the channel wrapper
connection.prep();
connectionManager.onConnected(connection);
}
@ -426,6 +451,13 @@ class EndPoint extends Shutdownable {
public abstract
ConnectionBridgeBase send();
/**
* Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol
* is available to use. If you want specify the protocol, use {@link #send()}, followed by the protocol you wish to use.
*/
public abstract
ConnectionPoint send(final Object message);
/**
* Closes all connections ONLY (keeps the server/client running). To STOP the client/server, use stop().
* <p/>
@ -434,17 +466,7 @@ class EndPoint extends Shutdownable {
* The server should ALWAYS use STOP.
*/
void closeConnections(boolean shouldKeepListeners) {
// give a chance to other threads.
Thread.yield();
// stop does the same as this + more. Only keep the listeners for connections IF we are the client. If we remove listeners as a client,
// ALL of the client logic will be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup)
connectionManager.closeConnections(shouldKeepListeners);
// Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed.
registrationWrapper.clearSessions();
isConnected.set(false);
}
/**
@ -462,8 +484,6 @@ class EndPoint extends Shutdownable {
@Override
protected
void shutdownChannelsPre() {
closeConnections(false);
// this does a closeConnections + clear_listeners
connectionManager.stop();
}

View File

@ -55,21 +55,23 @@ class EndPointClient extends EndPoint {
super(Client.class, config);
}
/**
* Internal call by the pipeline to start the client registering the different session protocols.
*/
protected
void startRegistration() throws IOException {
synchronized (bootstrapLock) {
// always reset everything.
registration = new CountDownLatch(1);
bootstrapIterator = bootstraps.iterator();
doRegistration();
}
doRegistration();
// have to BLOCK
// don't want the client to run before registration is complete
// have to BLOCK (must be outside of the synchronize call), we don't want the client to run before registration is complete
try {
if (!registration.await(connectionTimeout, TimeUnit.MILLISECONDS)) {
closeConnection();
throw new IOException("Unable to complete registration within '" + connectionTimeout + "' milliseconds");
}
} catch (InterruptedException e) {
@ -77,63 +79,6 @@ class EndPointClient extends EndPoint {
}
}
// this is called by 2 threads. The startup thread, and the registration-in-progress thread
private void doRegistration() {
synchronized (bootstrapLock) {
BootstrapWrapper bootstrapWrapper = bootstrapIterator.next();
ChannelFuture future;
if (connectionTimeout != 0) {
// must be before connect
bootstrapWrapper.bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout);
}
try {
// UDP : When this is CONNECT, a udp socket will ONLY accept UDP traffic from the remote address (ip/port combo).
// If the reply isn't from the correct port, then the other end will receive a "Port Unreachable" exception.
future = bootstrapWrapper.bootstrap.connect();
future.await(connectionTimeout);
} catch (Exception e) {
String errorMessage = "Could not connect to the " + bootstrapWrapper.type + " server at " + bootstrapWrapper.address + " on port: " + bootstrapWrapper.port;
if (logger.isDebugEnabled()) {
// extra info if debug is enabled
logger.error(errorMessage, e);
}
else {
logger.error(errorMessage);
}
return;
}
if (!future.isSuccess()) {
Throwable cause = future.cause();
// extra space here is so it aligns with "Connecting to server:"
String errorMessage = "Connection refused :" + bootstrapWrapper.address + " at " + bootstrapWrapper.type + " port: " + bootstrapWrapper.port;
if (cause instanceof java.net.ConnectException) {
if (cause.getMessage()
.contains("refused")) {
logger.error(errorMessage);
}
} else {
logger.error(errorMessage, cause);
}
return;
}
logger.trace("Waiting for registration from server.");
manageForShutdown(future);
}
}
/**
* Internal call by the pipeline to notify the client to continue registering the different session protocols.
*
@ -164,6 +109,69 @@ class EndPointClient extends EndPoint {
}
}
/**
* this is called by 2 threads. The startup thread, and the registration-in-progress thread
*
* NOTE: must be inside synchronize(bootstrapLock)!
*/
private
void doRegistration() {
BootstrapWrapper bootstrapWrapper = bootstrapIterator.next();
ChannelFuture future;
if (connectionTimeout != 0) {
// must be before connect
bootstrapWrapper.bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout);
}
try {
// UDP : When this is CONNECT, a udp socket will ONLY accept UDP traffic from the remote address (ip/port combo).
// If the reply isn't from the correct port, then the other end will receive a "Port Unreachable" exception.
future = bootstrapWrapper.bootstrap.connect();
future.await(connectionTimeout);
} catch (Exception e) {
String errorMessage =
"Could not connect to the " + bootstrapWrapper.type + " server at " + bootstrapWrapper.address + " on port: " +
bootstrapWrapper.port;
if (logger.isDebugEnabled()) {
// extra info if debug is enabled
logger.error(errorMessage, e);
}
else {
logger.error(errorMessage);
}
return;
}
if (!future.isSuccess()) {
Throwable cause = future.cause();
// extra space here is so it aligns with "Connecting to server:"
String errorMessage = "Connection refused :" + bootstrapWrapper.address + " at " + bootstrapWrapper.type + " port: " +
bootstrapWrapper.port;
if (cause instanceof java.net.ConnectException) {
if (cause.getMessage()
.contains("refused")) {
logger.error(errorMessage);
}
}
else {
logger.error(errorMessage, cause);
}
return;
}
logger.trace("Waiting for registration from server.");
manageForShutdown(future);
}
/**
* Internal (to the networking stack) to notify the client that registration has COMPLETED. This is necessary because the client
* will BLOCK until it has successfully registered it's connections.
@ -174,16 +182,18 @@ class EndPointClient extends EndPoint {
connectionBridgeFlushAlways = new ConnectionBridge() {
@Override
public
void self(Object message) {
connection.self(message);
flush();
ConnectionPoint self(Object message) {
ConnectionPoint self = connection.self(message);
connection.flush();
return self;
}
@Override
public
ConnectionPoint TCP(Object message) {
ConnectionPoint tcp = connection.TCP(message);
flush();
connection.flush();
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
// INSIDE the event loop
@ -196,7 +206,7 @@ class EndPointClient extends EndPoint {
public
ConnectionPoint UDP(Object message) {
ConnectionPoint udp = connection.UDP(message);
flush();
connection.flush();
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
// INSIDE the event loop
@ -208,25 +218,15 @@ class EndPointClient extends EndPoint {
public
Ping ping() {
Ping ping = connection.ping();
flush();
return ping;
}
@Override
public
void flush() {
connection.flush();
return ping;
}
};
//noinspection unchecked
this.connection = connection;
synchronized (bootstrapLock) {
// we're done with registration, so no need to keep this around
bootstrapIterator = null;
registration.countDown();
}
stopRegistration();
// invokes the listener.connection() method, and initialize the connection channels with whatever extra info they might need.
// This will also start the RMI (if necessary) initialization/creation of objects
@ -240,10 +240,23 @@ class EndPointClient extends EndPoint {
synchronized (bootstrapLock) {
// we're done with registration, so no need to keep this around
bootstrapIterator = null;
registration.countDown();
while (registration.getCount() > 0) {
registration.countDown();
}
}
}
/**
* AFTER registration is complete, if we are UDP only -- setup a heartbeat (must be the larger of 2x the idle timeout OR 10 seconds)
*
* If the server disconnects because of a heartbeat failure, the client has to be made aware of this when it tries to send data again
* (and it must go through it's entire reconnect protocol)
*/
protected
void startUdpHeartbeat() {
}
/**
* Expose methods to send objects to a destination.
* <p/>
@ -256,23 +269,48 @@ class EndPointClient extends EndPoint {
return connectionBridgeFlushAlways;
}
@Override
public
ConnectionPoint send(final Object message) {
ConnectionPoint send = connection.send(message);
send.flush();
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
// INSIDE the event loop
((ConnectionImpl)connection).controlBackPressure(send);
return send;
}
/**
* Closes all connections ONLY (keeps the client running). To STOP the client, use stop().
* <p/>
* This is used, for example, when reconnecting to a server.
*/
public
void closeConnections() {
// Only keep the listeners for connections IF we are the client. If we remove listeners as a client,
// ALL of the client logic will be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup)
closeConnections(true);
// for the CLIENT only, we clear these connections! (the server only clears them on shutdown)
shutdownChannels();
protected
void closeConnection() {
if (isConnected.get()) {
// make sure we're not waiting on registration
stopRegistration();
connection = null;
// for the CLIENT only, we clear these connections! (the server only clears them on shutdown)
// make sure we're not waiting on registration
stopRegistration();
// stop does the same as this + more. Only keep the listeners for connections IF we are the client. If we remove listeners as a client,
// ALL of the client logic will be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup)
connectionManager.closeConnections(true);
// Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed.
registrationWrapper.clearSessions();
closeConnections(true);
shutdownAllChannels();
// shutdownEventLoops();
connection = null;
isConnected.set(false);
}
}
/**

View File

@ -40,6 +40,17 @@ class EndPointServer extends EndPoint {
return this.connectionManager;
}
/**
* Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol
* is available to use. If you want specify the protocol, use {@link #send()}, followed by the protocol you wish to use.
*/
@Override
public
ConnectionPoint send(final Object message) {
return this.connectionManager.send(message);
}
/**
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener,
* and ALL connections are notified of that listener.
@ -78,7 +89,7 @@ class EndPointServer extends EndPoint {
*/
public
void add(Connection connection) {
connectionManager.addConnection(connection);
connectionManager.addConnection0(connection);
}
/**

View File

@ -33,6 +33,11 @@ interface ISessionManager {
*/
void onIdle(ConnectionImpl connection);
/**
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
*/
void addConnection(ConnectionImpl connection);
/**
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
* <p>

View File

@ -25,7 +25,6 @@ import com.esotericsoftware.kryo.Kryo;
import dorkbox.network.pipeline.ByteBufInput;
import dorkbox.network.pipeline.ByteBufOutput;
import dorkbox.network.pipeline.MagicBytes;
import dorkbox.network.serialization.CryptoSerializationManager;
import dorkbox.util.bytes.BigEndian;
import dorkbox.util.bytes.OptimizeUtilsByteArray;
@ -40,7 +39,7 @@ import net.jpountz.lz4.LZ4FastDecompressor;
* Nothing in this class is thread safe
*/
public
class KryoExtra<C extends ICryptoConnection> extends Kryo {
class KryoExtra<C extends CryptoConnection> extends Kryo {
// snappycomp : 7.534 micros/op; 518.5 MB/s (output: 55.1%)
// snappyuncomp : 1.391 micros/op; 2808.1 MB/s
// lz4comp : 6.210 micros/op; 629.0 MB/s (output: 55.4%)
@ -52,7 +51,7 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
private final ByteBufOutput writer = new ByteBufOutput();
// volatile to provide object visibility for entire class
public volatile IRmiConnection connection;
public volatile RmiConnection connection;
private final GCMBlockCipher aesEngine = new GCMBlockCipher(new AESFastEngine());
@ -94,9 +93,6 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
// connection will always be NULL during connection initialization
this.connection = null;
// during INIT and handshake, we don't use connection encryption/compression
buffer.writeByte(0);
// write the object to the NORMAL output buffer!
writer.setBuffer(buffer);
@ -114,15 +110,15 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
ByteBuf inputBuf = buffer;
// read off the magic byte
final byte magicByte = buffer.readByte();
// read the object from the buffer.
reader.setBuffer(inputBuf);
return readClassAndObject(reader); // this properly sets the readerIndex, but only if it's the correct buffer
}
/**
* This is NOT ENCRYPTED (and is only done on the loopback connection!)
*/
public synchronized
void writeCompressed(final C connection, final ByteBuf buffer, final Object message) throws IOException {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
@ -180,10 +176,10 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
byte[] compressOutput = this.compressOutput;
int maxLengthLengthOffset = 5;
int maxLengthLengthOffset = 4; // length is never negative, so 4 is OK (5 means it's negative)
int maxCompressedLength = compressor.maxCompressedLength(length);
// add 5 so there is room to write the compressed size to the buffer
// add 4 so there is room to write the compressed size to the buffer
int maxCompressedLengthWithOffset = maxCompressedLength + maxLengthLengthOffset;
// lazy initialize the compression output buffer
@ -194,7 +190,7 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
}
// LZ4 compress. output offset max 5 bytes to leave room for length of tempOutput data
// LZ4 compress. output offset max 4 bytes to leave room for length of tempOutput data
int compressedLength = compressor.compress(inputArray, inputOffset, length, compressOutput, maxLengthLengthOffset, maxCompressedLength);
// bytes can now be written to, because our compressed data is stored in a temp array.
@ -209,13 +205,13 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
// now write the ORIGINAL (uncompressed) length to the front of the byte array (this is NOT THE BUFFER!). This is so we can use the FAST decompress version
OptimizeUtilsByteArray.writeInt(inputArray, length, true, inputOffset);
// write out the "magic" byte.
buffer.writeByte(MagicBytes.crypto);
// have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size
buffer.writeBytes(inputArray, inputOffset, compressedLength + lengthLength);
}
/**
* This is NOT ENCRYPTED (and is only done on the loopback connection!)
*/
public
Object readCompressed(final C connection, final ByteBuf buffer, int length) throws IOException {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
@ -228,15 +224,12 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
ByteBuf inputBuf = buffer;
// read off the magic byte
final byte magicByte = buffer.readByte();
// get the decompressed length (at the beginning of the array)
final int uncompressedLength = OptimizeUtilsByteBuf.readInt(buffer, true);
final int lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true); // because 1-5 bytes for the decompressed size
// have to adjust for the magic byte and uncompressed length
length = length - 1 - lengthLength;
// have to adjust for uncompressed length
length = length - lengthLength;
///////// decompress data -- as it's ALWAYS compressed
@ -363,10 +356,10 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
byte[] compressOutput = this.compressOutput;
int maxLengthLengthOffset = 5;
int maxLengthLengthOffset = 4; // length is never negative, so 4 is OK (5 means it's negative)
int maxCompressedLength = compressor.maxCompressedLength(length);
// add 5 so there is room to write the compressed size to the buffer
// add 4 so there is room to write the compressed size to the buffer
int maxCompressedLengthWithOffset = maxCompressedLength + maxLengthLengthOffset;
// lazy initialize the compression output buffer
@ -378,7 +371,7 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
// LZ4 compress. output offset max 5 bytes to leave room for length of tempOutput data
// LZ4 compress. output offset max 4 bytes to leave room for length of tempOutput data
int compressedLength = compressor.compress(inputArray, inputOffset, length, compressOutput, maxLengthLengthOffset, maxCompressedLength);
// bytes can now be written to, because our compressed data is stored in a temp array.
@ -394,7 +387,7 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
OptimizeUtilsByteArray.writeInt(inputArray, length, true, inputOffset);
// correct length for encryption
length = compressedLength + lengthLength; // +1 to +5 for the uncompressed size bytes
length = compressedLength + lengthLength; // +1 to +4 for the uncompressed size bytes
@ -432,9 +425,6 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
throw new IOException("Unable to AES encrypt the data", e);
}
// write out the "magic" byte.
buffer.writeByte(MagicBytes.crypto);
// write out our GCM counter
OptimizeUtilsByteBuf.writeLong(buffer, nextGcmSequence, true);
@ -454,15 +444,11 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
ByteBuf inputBuf = buffer;
// read off the magic byte
final byte magicByte = buffer.readByte();
final long gcmIVCounter = OptimizeUtilsByteBuf.readLong(buffer, true);
// compression can ONLY happen if it's ALSO crypto'd
// have to adjust for the magic byte and the gcmIVCounter
length = length - 1 - OptimizeUtilsByteArray.longLength(gcmIVCounter, true);
// have to adjust for the gcmIVCounter
length = length - OptimizeUtilsByteArray.longLength(gcmIVCounter, true);
/////////// decrypting data
@ -543,7 +529,7 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
// get the decompressed length (at the beginning of the array)
inputArray = decryptOutputArray;
final int uncompressedLength = OptimizeUtilsByteArray.readInt(inputArray, true);
inputOffset = OptimizeUtilsByteArray.intLength(uncompressedLength, true); // because 1-5 bytes for the decompressed size
inputOffset = OptimizeUtilsByteArray.intLength(uncompressedLength, true); // because 1-4 bytes for the decompressed size
byte[] decompressOutputArray = this.decompressOutput;

View File

@ -112,12 +112,11 @@ class RegistrationWrapper {
}
/**
* Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications to the pipeline
* are finished.
* Internal call by the pipeline to notify the "Connection" object that it has "connected".
*/
public
void connectionConnected0(ConnectionImpl networkConnection) {
this.endPoint.connectionConnected0(networkConnection);
void connectionConnected0(ConnectionImpl connection) {
this.endPoint.connectionConnected0(connection);
}
/**
@ -126,7 +125,7 @@ class RegistrationWrapper {
* @param metaChannel can be NULL (when getting the baseClass)
*/
public
Connection connection0(MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
ConnectionImpl connection0(MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
return this.endPoint.connection0(metaChannel, remoteAddress);
}

View File

@ -21,7 +21,7 @@ import dorkbox.network.rmi.RemoteObject;
* Supporting methods for RMI connections
*/
public
interface IRmiConnection {
interface RmiConnection {
/**
* Used by RMI for the LOCAL side, to get the proxy object as an interface

View File

@ -1,5 +1,10 @@
package dorkbox.network.connection;
import static dorkbox.network.pipeline.ConnectionType.EPOLL;
import static dorkbox.network.pipeline.ConnectionType.KQUEUE;
import static dorkbox.network.pipeline.ConnectionType.NIO;
import static dorkbox.network.pipeline.ConnectionType.OIO;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.LinkedList;
@ -10,11 +15,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import dorkbox.network.NativeLibrary;
import dorkbox.network.pipeline.ConnectionType;
import dorkbox.util.NamedThreadFactory;
import dorkbox.util.OS;
import dorkbox.util.Property;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.oio.OioEventLoopGroup;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.internal.PlatformDependent;
@ -71,6 +84,31 @@ class Shutdownable {
@Property
public static long maxShutdownWaitTimeInMilliSeconds = 2000L; // in milliseconds
/**
* Checks to see if we are running in the netty thread. This is (usually) to prevent potential deadlocks in code that CANNOT be run from
* inside a netty worker.
*/
public static
boolean isNettyThread() {
return Thread.currentThread()
.getThreadGroup()
.getName()
.contains(THREADGROUP_NAME);
}
/**
* Runs a runnable inside a NEW thread that is NOT in the same thread group as Netty
*/
public static
void runNewThread(final String threadName, final Runnable runnable) {
Thread thread = new Thread(Thread.currentThread()
.getThreadGroup()
.getParent(),
runnable);
thread.setDaemon(true);
thread.setName(threadName);
thread.start();
}
protected final org.slf4j.Logger logger;
@ -145,8 +183,18 @@ class Shutdownable {
}
}
/**
* Remove an eventloop group to be tracked & managed for shutdown
*/
protected final
void removeFromShutdown(EventLoopGroup loopGroup) {
synchronized (eventLoopGroups) {
eventLoopGroups.remove(loopGroup);
}
}
// server only does this on stop. Client does this on closeConnections
void shutdownChannels() {
void shutdownAllChannels() {
synchronized (shutdownChannelList) {
// now we stop all of our channels. For the server, this will close the server manager for UDP sessions
for (ChannelFuture f : shutdownChannelList) {
@ -163,6 +211,31 @@ class Shutdownable {
}
}
// shutdown all event loops associated
void shutdownEventLoops() {
// we want to WAIT until after the event executors have completed shutting down.
List<Future<?>> shutdownThreadList = new LinkedList<Future<?>>();
List<EventLoopGroup> loopGroups;
synchronized (eventLoopGroups) {
loopGroups = new ArrayList<EventLoopGroup>(eventLoopGroups.size());
loopGroups.addAll(eventLoopGroups);
}
for (EventLoopGroup loopGroup : loopGroups) {
shutdownThreadList.add(loopGroup.shutdownGracefully(maxShutdownWaitTimeInMilliSeconds,
maxShutdownWaitTimeInMilliSeconds * 10,
TimeUnit.MILLISECONDS));
Thread.yield();
}
// now wait for them to finish!
// It can take a few seconds to shut down the executor. This will affect unit testing, where connections are quickly created/stopped
for (Future<?> f : shutdownThreadList) {
f.syncUninterruptibly();
Thread.yield();
}
}
protected final
String stopWithErrorMessage(Logger logger, String errorMessage, Throwable throwable) {
@ -189,6 +262,72 @@ class Shutdownable {
}
/**
* Creates a new event loop based on the OS type and specified configuration
*
* @param threadCount number of threads for the event loop
*
* @return a new event loop group based on the specified parameters
*/
protected
EventLoopGroup newEventLoop(final int threadCount, final String threadName) {
if (OS.isAndroid()) {
// android ONLY supports OIO
return newEventLoop(OIO, threadCount, threadName);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// epoll network stack is MUCH faster (but only on linux)
return newEventLoop(EPOLL, threadCount, threadName);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// KQueue network stack is MUCH faster (but only on macosx)
return newEventLoop(KQUEUE, threadCount, threadName);
}
else {
return newEventLoop(NIO, threadCount, threadName);
}
}
/**
* Creates a new event loop based on the specified configuration
*
* @param connectionType LOCAL, NIO, EPOLL, etc...
* @param threadCount number of threads for the event loop
*
* @return a new event loop group based on the specified parameters
*/
protected
EventLoopGroup newEventLoop(final ConnectionType connectionType, final int threadCount, final String threadName) {
NamedThreadFactory threadFactory = new NamedThreadFactory(threadName, threadGroup);
EventLoopGroup group;
switch (connectionType) {
case LOCAL:
group = new DefaultEventLoopGroup(threadCount, threadFactory);
break;
case OIO:
group = new OioEventLoopGroup(threadCount, threadFactory);
break;
case NIO:
group = new NioEventLoopGroup(threadCount, threadFactory);
break;
case EPOLL:
group = new EpollEventLoopGroup(threadCount, threadFactory);
break;
case KQUEUE:
group = new KQueueEventLoopGroup(threadCount, threadFactory);
break;
default:
group = new DefaultEventLoopGroup(threadCount, threadFactory);
break;
}
manageForShutdown(group);
return group;
}
/**
* Check to see if the current thread is running from it's OWN thread, or from Netty... This is used to prevent deadlocks.
*
@ -276,32 +415,9 @@ class Shutdownable {
// This will wait until we have finished starting up/shutting down.
synchronized (shutdownInProgress) {
shutdownChannelsPre();
shutdownChannels();
// we want to WAIT until after the event executors have completed shutting down.
List<Future<?>> shutdownThreadList = new LinkedList<Future<?>>();
List<EventLoopGroup> loopGroups;
synchronized (eventLoopGroups) {
loopGroups = new ArrayList<EventLoopGroup>(eventLoopGroups.size());
loopGroups.addAll(eventLoopGroups);
}
for (EventLoopGroup loopGroup : loopGroups) {
shutdownThreadList.add(loopGroup.shutdownGracefully(maxShutdownWaitTimeInMilliSeconds,
maxShutdownWaitTimeInMilliSeconds * 4,
TimeUnit.MILLISECONDS));
Thread.yield();
}
// now wait for them to finish!
// It can take a few seconds to shut down the executor. This will affect unit testing, where connections are quickly created/stopped
for (Future<?> f : shutdownThreadList) {
f.syncUninterruptibly();
Thread.yield();
}
shutdownAllChannels();
shutdownEventLoops();
logger.info("Stopping endpoint.");

View File

@ -25,9 +25,4 @@ interface ConnectionBridge extends ConnectionBridgeBase {
* @return Ping can have a listener attached, which will get called when the ping returns.
*/
Ping ping();
/**
* Flushes the contents of the TCP/UDP/etc pipes to the actual transport.
*/
void flush();
}

View File

@ -22,7 +22,7 @@ interface ConnectionBridgeBase {
/**
* Sends the message to other listeners INSIDE this endpoint. It does not send it to a remote address.
*/
void self(Object message);
ConnectionPoint self(Object message);
/**
* Sends the message over the network using TCP. (or via LOCAL when it's a local channel).

View File

@ -0,0 +1,188 @@
/*
* Copyright 2018 dorkbox, llc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.registration;
import org.bouncycastle.crypto.params.ParametersWithIV;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.ConnectionPoint;
import dorkbox.network.connection.CryptoConnection;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Listeners;
import dorkbox.network.connection.bridge.ConnectionBridge;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleSender;
import dorkbox.network.rmi.RemoteObject;
import dorkbox.network.rmi.RemoteObjectCallback;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
/**
* A wrapper for the period of time between registration and connect for a "connection" session.
*
* This is to prevent race conditions where onMessage() can happen BEFORE a "connection" is "connected"
*/
public
class ConnectionWrapper implements CryptoConnection, ChannelHandler {
public final ConnectionImpl connection;
public
ConnectionWrapper(final ConnectionImpl connection) {
this.connection = connection;
}
@Override
public
void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
}
@Override
public
void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
}
@Override
public
void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
}
@Override
public
long getNextGcmSequence() {
return connection.getNextGcmSequence();
}
@Override
public
ParametersWithIV getCryptoParameters() {
return connection.getCryptoParameters();
}
@Override
public
RemoteObject getProxyObject(final int objectID, final Class<?> iFace) {
throw new IllegalArgumentException("not implemented");
}
@Override
public
Object getImplementationObject(final int objectID) {
throw new IllegalArgumentException("not implemented");
}
@Override
public
<T> int getRegisteredId(final T object) {
return 0;
}
@Override
public
boolean hasRemoteKeyChanged() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
String getRemoteHost() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
boolean isLoopback() {
return connection.isLoopback();
}
@Override
public
EndPoint getEndPoint() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
int id() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
String idAsHex() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
boolean hasUDP() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
ConnectionBridge send() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
ConnectionPoint send(final Object message) {
throw new IllegalArgumentException("not implemented");
}
@Override
public
IdleBridge sendOnIdle(final IdleSender<?, ?> sender) {
throw new IllegalArgumentException("not implemented");
}
@Override
public
IdleBridge sendOnIdle(final Object message) {
throw new IllegalArgumentException("not implemented");
}
@Override
public
Listeners listeners() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
void close() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
void closeAsap() {
throw new IllegalArgumentException("not implemented");
}
@Override
public
<Iface> void createRemoteObject(final Class<Iface> interfaceClass, final RemoteObjectCallback<Iface> callback) {
throw new IllegalArgumentException("not implemented");
}
@Override
public
<Iface> void getRemoteObject(final int objectId, final RemoteObjectCallback<Iface> callback) {
throw new IllegalArgumentException("not implemented");
}
}

View File

@ -20,12 +20,11 @@ import java.util.concurrent.atomic.AtomicLong;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import dorkbox.network.connection.ConnectionImpl;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
public
class MetaChannel {
// @formatter:off
// 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.
@ -39,7 +38,7 @@ class MetaChannel {
public Channel tcpChannel = null;
public Channel udpChannel = null;
public ConnectionImpl connection; // only needed until the connection has been notified.
public ChannelHandler 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
@ -53,60 +52,11 @@ class MetaChannel {
// If the server detects this, it has the option for additional security (two-factor auth, perhaps?)
public boolean changedRemoteKey = false;
// @formatter:on
public
MetaChannel(final int sessionId) {
this.sessionId = sessionId;
}
public
void close() {
if (this.localChannel != null) {
this.localChannel.close();
}
if (this.tcpChannel != null) {
this.tcpChannel.close();
}
if (this.udpChannel != null) {
// if (this.udpRemoteAddress == null) {
// FIXME: ?? only the CLIENT will have this.
this.udpChannel.close();
// }
// else if (this.handlerServerUDP != null) {
// only the SERVER will have this
// we DO NOT want to close the UDP channel, otherwise no other UDP clients can connect
// this.handlerServerUDP.unRegisterServerUDP(this.udpRemoteAddress);
// }
}
}
public
void close(final long maxShutdownWaitTimeInMilliSeconds) {
if (this.localChannel != null && this.localChannel.isOpen()) {
this.localChannel.close();
}
if (this.tcpChannel != null && this.tcpChannel.isOpen()) {
this.tcpChannel.close()
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
}
if (this.udpChannel != null && this.udpChannel.isOpen()) {
// if (this.udpRemoteAddress == null) {
// FIXME: ?? only the CLIENT will have this.
this.udpChannel.close()
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
// }
// else {
// only the SERVER will have this
// we DO NOT want to close the UDP channel, otherwise no other UDP clients can connect
// this.handlerServerUDP.unRegisterServerUDP(this.udpRemoteAddress);
// }
}
}
/**
* Update the network round trip time.

View File

@ -35,6 +35,12 @@ class Registration {
// true if we have more registrations to process, false if we are done
public boolean hasMore;
// true when we are ready to setup the connection (hasMore will always be false if this is true). False when we are ready to connect
public boolean upgrade;
// true when we are fully upgraded
public boolean upgraded;
private
Registration() {
// for serialization

View File

@ -20,19 +20,22 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.EventLoopGroup;
@Sharable
public abstract
class RegistrationHandler extends ChannelInboundHandlerAdapter {
protected static final String CONNECTION_HANDLER = "connectionHandler";
protected static final String CONNECTION_HANDLER = "connection";
protected final RegistrationWrapper registrationWrapper;
protected final org.slf4j.Logger logger;
protected final String name;
protected final EventLoopGroup workerEventLoop;
public
RegistrationHandler(final String name, RegistrationWrapper registrationWrapper) {
RegistrationHandler(final String name, RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) {
this.name = name;
this.workerEventLoop = workerEventLoop;
this.logger = org.slf4j.LoggerFactory.getLogger(this.name);
this.registrationWrapper = registrationWrapper;
}
@ -74,7 +77,6 @@ class RegistrationHandler extends ChannelInboundHandlerAdapter {
@Override
public
void channelReadComplete(final ChannelHandlerContext context) throws Exception {
context.flush();
}
@Override

View File

@ -20,14 +20,15 @@ import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.RegistrationHandler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.EventLoopGroup;
import io.netty.util.AttributeKey;
public abstract
class RegistrationLocalHandler extends RegistrationHandler {
public static final AttributeKey<MetaChannel> META_CHANNEL = AttributeKey.valueOf(RegistrationLocalHandler.class, "MetaChannel.local");
RegistrationLocalHandler(String name, RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationLocalHandler(String name, RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
}
/**
@ -45,6 +46,12 @@ class RegistrationLocalHandler extends RegistrationHandler {
logger.trace("New LOCAL connection.");
}
@Override
public
void channelActive(final ChannelHandlerContext context) throws Exception {
// to suppress warnings in the super class
}
@Override
public
void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
@ -59,12 +66,6 @@ class RegistrationLocalHandler extends RegistrationHandler {
}
}
@Override
public
void channelActive(ChannelHandlerContext context) throws Exception {
// not used (so we prevent the warnings from the super class)
}
// this SHOULDN'T ever happen, but we might shutdown in the middle of registration
@Override
public final

View File

@ -22,14 +22,15 @@ import dorkbox.network.connection.registration.Registration;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.util.ReferenceCountUtil;
public
class RegistrationLocalHandlerClient extends RegistrationLocalHandler {
public
RegistrationLocalHandlerClient(String name, RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationLocalHandlerClient(String name, RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
}
/**
@ -67,13 +68,10 @@ class RegistrationLocalHandlerClient extends RegistrationLocalHandler {
// Event though a local channel is XOR with everything else, we still have to make the client clean up it's state.
registrationWrapper.startNextProtocolRegistration();
registrationWrapper.connection0(metaChannel, null);
ConnectionImpl connection = metaChannel.connection;
ConnectionImpl connection = registrationWrapper.connection0(metaChannel, null);
// have to setup connection handler
pipeline.addLast(CONNECTION_HANDLER, connection);
registrationWrapper.connectionConnected0(connection);
}
else {

View File

@ -21,14 +21,15 @@ import dorkbox.network.connection.registration.MetaChannel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.util.ReferenceCountUtil;
public
class RegistrationLocalHandlerServer extends RegistrationLocalHandler {
public
RegistrationLocalHandlerServer(String name, RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationLocalHandlerServer(String name, RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
}
/**
@ -65,13 +66,11 @@ class RegistrationLocalHandlerServer extends RegistrationLocalHandler {
MetaChannel metaChannel = channel.attr(META_CHANNEL)
.getAndSet(null);
if (metaChannel != null) {
registrationWrapper.connection0(metaChannel, null);
ConnectionImpl connection = metaChannel.connection;
ConnectionImpl connection = registrationWrapper.connection0(metaChannel, null);
if (connection != null) {
// have to setup connection handler
pipeline.addLast(CONNECTION_HANDLER, connection);
registrationWrapper.connectionConnected0(connection);
}
}

View File

@ -22,7 +22,9 @@ import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.ConnectionWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.connection.registration.RegistrationHandler;
@ -31,11 +33,15 @@ import dorkbox.network.pipeline.tcp.KryoDecoderCrypto;
import dorkbox.network.serialization.CryptoSerializationManager;
import dorkbox.util.crypto.CryptoECC;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
public abstract
class RegistrationRemoteHandler extends RegistrationHandler {
@ -59,8 +65,8 @@ class RegistrationRemoteHandler extends RegistrationHandler {
protected final CryptoSerializationManager serializationManager;
RegistrationRemoteHandler(final String name, final RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationRemoteHandler(final String name, final RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
this.serializationManager = registrationWrapper.getSerializtion();
}
@ -76,8 +82,8 @@ class RegistrationRemoteHandler extends RegistrationHandler {
Class<? extends Channel> channelClass = channel.getClass();
// because of the way TCP works, we have to have special readers/writers. For UDP, all data must be in a single packet.
boolean isTcpChannel = ConnectionImpl.isTcp(channelClass);
boolean isUdpChannel = !isTcpChannel && ConnectionImpl.isUdp(channelClass);
boolean isTcpChannel = ConnectionImpl.isTcpChannel(channelClass);
boolean isUdpChannel = !isTcpChannel && ConnectionImpl.isUdpChannel(channelClass);
if (isTcpChannel) {
///////////////////////
@ -91,14 +97,10 @@ class RegistrationRemoteHandler extends RegistrationHandler {
pipeline.addFirst(KRYO_DECODER, this.registrationWrapper.kryoUdpDecoder);
}
int idleTimeout = this.registrationWrapper.getIdleTimeout();
if (idleTimeout > 0) {
// this makes the proper event get raised in the registrationHandler to kill NEW idle connections. Once "connected" they last a lot longer.
// we ALWAYS have this initial IDLE handler, so we don't have to worry about a SLOW-LORIS ATTACK against the server.
// in Seconds -- not shared, because it is per-connection
pipeline.addFirst(IDLE_HANDLER, new IdleStateHandler(2, 0, 0));
}
// this makes the proper event get raised in the registrationHandler to kill NEW idle connections. Once "connected" they last a lot longer.
// we ALWAYS have this initial IDLE handler, so we don't have to worry about a SLOW-LORIS ATTACK against the server.
// in Seconds -- not shared, because it is per-connection
pipeline.addFirst(IDLE_HANDLER, new IdleStateHandler(2, 0, 0));
if (isTcpChannel) {
/////////////////////////
@ -120,49 +122,35 @@ class RegistrationRemoteHandler extends RegistrationHandler {
// add the channel so we can access it later.
// do NOT want to add UDP channels, since they are tracked differently.
if (this.logger.isInfoEnabled()) {
if (this.logger.isDebugEnabled()) {
Channel channel = context.channel();
Class<? extends Channel> channelClass = channel.getClass();
boolean isUdp = ConnectionImpl.isUdp(channelClass);
boolean isUdp = ConnectionImpl.isUdpChannel(channelClass);
StringBuilder stringBuilder = new StringBuilder(96);
stringBuilder.append("Connected to remote ");
if (ConnectionImpl.isTcp(channelClass)) {
if (ConnectionImpl.isTcpChannel(channelClass)) {
stringBuilder.append("TCP");
}
else if (isUdp) {
stringBuilder.append("UDP");
}
else if (ConnectionImpl.isLocal(channelClass)) {
else if (ConnectionImpl.isLocalChannel(channelClass)) {
stringBuilder.append("LOCAL");
}
else {
stringBuilder.append("UNKNOWN");
}
stringBuilder.append(" connection. [");
stringBuilder.append(channel.localAddress());
stringBuilder.append(" connection [");
EndPoint.getHostDetails(stringBuilder, channel.localAddress());
// this means we are "Sessionless"
if (isUdp) {
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(getConnectionDirection());
EndPoint.getHostDetails(stringBuilder, channel.remoteAddress());
stringBuilder.append("]");
this.logger.info(stringBuilder.toString());
this.logger.debug(stringBuilder.toString());
}
}
@ -221,119 +209,228 @@ class RegistrationRemoteHandler extends RegistrationHandler {
return false;
}
// have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready.
/**
* upgrades a channel ONE channel at a time
*/
final
void setupConnectionCrypto(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
void upgradeDecoders(final Channel channel, final MetaChannel metaChannel) {
ChannelPipeline pipeline = channel.pipeline();
if (this.logger.isDebugEnabled()) {
String type = "";
try {
if (metaChannel.tcpChannel == channel) {
// add the new handlers (FORCE encryption and longer IDLE handler)
pipeline.replace(FRAME_AND_KRYO_DECODER,
FRAME_AND_KRYO_CRYPTO_DECODER,
new KryoDecoderCrypto(this.serializationManager)); // cannot be shared because of possible fragmentation.
}
if (metaChannel.tcpChannel != null) {
type = "TCP";
if (metaChannel.udpChannel == channel) {
if (metaChannel.tcpChannel == null) {
// TODO: UDP (and TCP??) idle timeout (also, to close UDP session when TCP shuts down)
// this means that we are ONLY UDP, and we should have an idle timeout that CLOSES the session after a while.
// Naturally, one would want the "normal" idle to trigger first, but there always be a heartbeat if the idle trigger DOES NOT
// send data on the network, to make sure that the UDP-only session stays alive or disconnects.
if (metaChannel.udpChannel != null) {
type += "/";
// If the server disconnects, the client has to be made aware of this when it tries to send data again (it must go through
// it's entire reconnect protocol)
}
// these encoders are shared
pipeline.replace(KRYO_DECODER, KRYO_CRYPTO_DECODER, this.registrationWrapper.kryoUdpDecoderCrypto);
}
if (metaChannel.udpChannel != null) {
type += "UDP";
}
this.logger.debug("Encrypting {} session with {}", type, remoteAddress);
}
if (metaChannel.tcpChannel != null) {
ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
// add the new handlers (FORCE encryption and longer IDLE handler)
pipeline.replace(FRAME_AND_KRYO_DECODER,
FRAME_AND_KRYO_CRYPTO_DECODER,
new KryoDecoderCrypto(this.serializationManager)); // cannot be shared because of possible fragmentation.
int idleTimeout = this.registrationWrapper.getIdleTimeout();
if (idleTimeout > 0) {
pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS));
}
pipeline.replace(FRAME_AND_KRYO_ENCODER,
FRAME_AND_KRYO_CRYPTO_ENCODER,
this.registrationWrapper.kryoTcpEncoderCrypto); // this is shared
}
if (metaChannel.udpChannel != null) {
ChannelPipeline pipeline = metaChannel.udpChannel.pipeline();
int idleTimeout = this.registrationWrapper.getIdleTimeout();
if (idleTimeout > 0) {
pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS));
}
pipeline.replace(KRYO_DECODER, KRYO_CRYPTO_DECODER, this.registrationWrapper.kryoUdpDecoderCrypto);
pipeline.replace(KRYO_ENCODER, KRYO_CRYPTO_ENCODER, this.registrationWrapper.kryoUdpEncoderCrypto);
}
}
// have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready.
@SuppressWarnings("AutoUnboxing")
final
void setupConnection(final MetaChannel metaChannel, final Channel channel) {
// Now setup our meta-channel to migrate to the correct connection handler for all regular data.
// add the "connected"/"normal" handler now that we have established a "new" connection.
// This will have state, etc. for this connection.
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
ConnectionImpl connection = (ConnectionImpl) this.registrationWrapper.connection0(metaChannel, remoteAddress);
if (metaChannel.tcpChannel != null) {
ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
if (registrationWrapper.isClient()) {
pipeline.remove(RegistrationRemoteHandlerClientTCP.class);
}
else {
pipeline.remove(RegistrationRemoteHandlerServerTCP.class);
}
pipeline.addLast(CONNECTION_HANDLER, connection);
}
if (metaChannel.udpChannel != null) {
ChannelPipeline pipeline = metaChannel.udpChannel.pipeline();
if (registrationWrapper.isClient()) {
pipeline.remove(RegistrationRemoteHandlerClientUDP.class);
}
else {
pipeline.remove(RegistrationRemoteHandlerServerUDP.class);
}
pipeline.addLast(CONNECTION_HANDLER, connection);
}
if (this.logger.isInfoEnabled()) {
String type = "";
if (metaChannel.tcpChannel != null) {
type = "TCP";
if (metaChannel.udpChannel != null) {
type += "/";
}
}
if (metaChannel.udpChannel != null) {
type += "UDP";
}
this.logger.info("Created a {} connection with {}", type, remoteAddress.getAddress());
} catch (Exception e) {
logger.error("Error during connection pipeline upgrade", e);
}
}
/**
* Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications to the pipeline
* are finished.
* upgrades a channel ONE channel at a time
*/
final
void notifyConnection(MetaChannel metaChannel) {
this.registrationWrapper.connectionConnected0(metaChannel.connection);
this.registrationWrapper.removeSession(metaChannel);
void upgradeEncoders(final Channel channel, final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
ChannelPipeline pipeline = channel.pipeline();
try {
if (metaChannel.tcpChannel == channel) {
// add the new handlers (FORCE encryption and longer IDLE handler)
pipeline.replace(FRAME_AND_KRYO_ENCODER,
FRAME_AND_KRYO_CRYPTO_ENCODER,
registrationWrapper.kryoTcpEncoderCrypto); // this is shared
}
if (metaChannel.udpChannel == channel) {
// these encoders are shared
pipeline.replace(KRYO_ENCODER, KRYO_CRYPTO_ENCODER, registrationWrapper.kryoUdpEncoderCrypto);
}
} catch (Exception e) {
logger.error("Error during connection pipeline upgrade", e);
}
}
/**
* upgrades a channel ONE channel at a time
*/
final
void upgradePipeline(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
try {
if (metaChannel.udpChannel != null) {
if (metaChannel.tcpChannel == null) {
// TODO: UDP (and TCP??) idle timeout (also, to close UDP session when TCP shuts down)
// this means that we are ONLY UDP, and we should have an idle timeout that CLOSES the session after a while.
// Naturally, one would want the "normal" idle to trigger first, but there always be a heartbeat if the idle trigger DOES NOT
// send data on the network, to make sure that the UDP-only session stays alive or disconnects.
// If the server disconnects, the client has to be made aware of this when it tries to send data again (it must go through
// it's entire reconnect protocol)
}
}
// add the "connected"/"normal" handler now that we have established a "new" connection.
// This will have state, etc. for this connection. THIS MUST BE 100% TCP/UDP created, otherwise it will break connections!
ConnectionImpl connection = this.registrationWrapper.connection0(metaChannel, remoteAddress);
metaChannel.connection = new ConnectionWrapper(connection);
// Now setup our meta-channel to migrate to the correct connection handler for all regular data.
if (metaChannel.tcpChannel != null) {
final ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
pipeline.addLast(CONNECTION_HANDLER, metaChannel.connection);
}
if (metaChannel.udpChannel != null) {
final ChannelPipeline pipeline = metaChannel.udpChannel.pipeline();
pipeline.addLast(CONNECTION_HANDLER, metaChannel.connection);
}
if (this.logger.isInfoEnabled()) {
String type = "";
if (metaChannel.tcpChannel != null) {
type = "TCP";
if (metaChannel.udpChannel != null) {
type += "/";
}
}
if (metaChannel.udpChannel != null) {
type += "UDP";
}
StringBuilder stringBuilder = new StringBuilder(96);
stringBuilder.append("Encrypted ");
if (metaChannel.tcpChannel != null) {
stringBuilder.append(type)
.append(" connection [");
EndPoint.getHostDetails(stringBuilder, metaChannel.tcpChannel.localAddress());
stringBuilder.append(getConnectionDirection());
EndPoint.getHostDetails(stringBuilder, metaChannel.tcpChannel.remoteAddress());
stringBuilder.append("]");
}
else if (metaChannel.udpChannel != null) {
stringBuilder.append(type)
.append(" connection [");
EndPoint.getHostDetails(stringBuilder, metaChannel.udpChannel.localAddress());
stringBuilder.append(getConnectionDirection());
EndPoint.getHostDetails(stringBuilder, metaChannel.udpChannel.remoteAddress());
stringBuilder.append("]");
}
this.logger.info(stringBuilder.toString());
}
} catch (Exception e) {
logger.error("Error during connection pipeline upgrade", e);
}
}
final void cleanupPipeline(final MetaChannel metaChannel) {
final int idleTimeout = this.registrationWrapper.getIdleTimeout();
try {
// REMOVE our channel wrapper (only used for encryption) with the actual connection
metaChannel.connection = ((ConnectionWrapper) metaChannel.connection).connection;
if (metaChannel.tcpChannel != null) {
final ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
if (registrationWrapper.isClient()) {
pipeline.remove(RegistrationRemoteHandlerClientTCP.class);
}
else {
pipeline.remove(RegistrationRemoteHandlerServerTCP.class);
}
pipeline.remove(ConnectionWrapper.class);
if (idleTimeout > 0) {
pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS));
} else {
pipeline.remove(IDLE_HANDLER);
}
pipeline.addLast(CONNECTION_HANDLER, metaChannel.connection);
// we also DEREGISTER and run on a different event loop!
ChannelFuture future = metaChannel.tcpChannel.deregister();
future.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public
void operationComplete(final Future<? super Void> f) throws Exception {
if (f.isSuccess()) {
workerEventLoop.register(metaChannel.tcpChannel);
}
}
});
}
if (metaChannel.udpChannel != null) {
final ChannelPipeline pipeline = metaChannel.udpChannel.pipeline();
if (registrationWrapper.isClient()) {
pipeline.remove(RegistrationRemoteHandlerClientUDP.class);
}
else {
pipeline.remove(RegistrationRemoteHandlerServerUDP.class);
}
pipeline.remove(ConnectionWrapper.class);
if (idleTimeout > 0) {
pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS));
}
else {
pipeline.remove(IDLE_HANDLER);
}
pipeline.addLast(CONNECTION_HANDLER, metaChannel.connection);
// we also DEREGISTER and run on a different event loop!
// ONLY necessary for UDP-CLIENT, because for UDP-SERVER, the SessionManager takes care of this!
// if (registrationWrapper.isClient()) {
// ChannelFuture future = metaChannel.udpChannel.deregister();
// future.addListener(new GenericFutureListener<Future<? super Void>>() {
// @Override
// public
// void operationComplete(final Future<? super Void> f) throws Exception {
// if (f.isSuccess()) {
// workerEventLoop.register(metaChannel.udpChannel);
// }
// }
// });
// }
}
} catch (Exception e) {
logger.error("Error during pipeline replace", e);
}
}
final
void doConnect(final MetaChannel metaChannel) {
// safe cast, because it's always this way...
this.registrationWrapper.connectionConnected0((ConnectionImpl) metaChannel.connection);
}
// whoa! Didn't send valid public key info!

View File

@ -36,12 +36,13 @@ import dorkbox.util.crypto.CryptoECC;
import dorkbox.util.exceptions.SecurityException;
import dorkbox.util.serialization.EccPublicKeySerializer;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
public
class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler {
RegistrationRemoteHandlerClient(final String name, final RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationRemoteHandlerClient(final String name, final RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
// check to see if we need to delete an IP address as commanded from the user prompt
String ipAsString = System.getProperty(DELETE_IP);
@ -92,7 +93,7 @@ class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler {
@SuppressWarnings("Duplicates")
void readClient(final Channel channel, final Registration registration, final String type, final MetaChannel metaChannel) {
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
final InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
// IN: session ID + public key + ecc parameters (which are a nonce. the SERVER defines what these are)
// OUT: remote ECDH shared payload
@ -124,7 +125,6 @@ class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler {
EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic());
outboundRegister.payload = output.toBytes();
metaChannel.updateRoundTripOnWrite();
channel.writeAndFlush(outboundRegister);
return;
}
@ -171,46 +171,59 @@ class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler {
Registration outboundRegister = new Registration(metaChannel.sessionId);
// do we have any more registrations?
boolean hasMoreRegistrations = registrationWrapper.hasMoreRegistrations();
outboundRegister.hasMore = hasMoreRegistrations;
metaChannel.updateRoundTripOnWrite();
outboundRegister.hasMore = registrationWrapper.hasMoreRegistrations();
channel.writeAndFlush(outboundRegister);
if (hasMoreRegistrations) {
// start the process for the next protocol.
registrationWrapper.startNextProtocolRegistration();
}
// always return!
// wait for ack from the server before registering the next protocol
return;
}
// // when we have a "continuing registration" for another protocol, we have to have another roundtrip.
// if (registration.payload != null) {
// metaChannel.updateRoundTripTime();
// channel.writeAndFlush(new Registration(metaChannel.sessionId));
// return;
// }
// IN: upgrade=true if we must upgrade this connection
if (registration.upgrade) {
// this pipeline can now be marked to be upgraded
// upgrade the connection to an encrypted connection
// this pipeline encoder/decoder can now be upgraded, and the "connection" added
upgradeEncoders(channel, metaChannel, remoteAddress);
upgradeDecoders(channel, metaChannel);
}
// IN: hasMore=true if we have more registrations to do, false otherwise
if (registration.hasMore) {
registrationWrapper.startNextProtocolRegistration();
return;
}
// We ONLY get here after the server acks our registration status.
// The server will only ack if we DO NOT have more registrations. If we have more registrations, the server waits.
setupConnectionCrypto(metaChannel, remoteAddress);
setupConnection(metaChannel, channel);
//
//
// we only get this when we are 100% done with the registration of all connection types.
//
//
// wait for a "round trip" amount of time, then notify the APP!
final long delay = TimeUnit.NANOSECONDS.toMillis(metaChannel.getRoundTripTime() * 2);
channel.eventLoop()
.schedule(new Runnable() {
@Override
public
void run() {
logger.trace("Notify Connection");
notifyConnection(metaChannel);
}
}, delay, TimeUnit.MILLISECONDS);
if (!registration.upgraded) {
// setup the pipeline with the real connection
upgradePipeline(metaChannel, remoteAddress);
// tell the server we are upgraded (it will bounce back telling us to connect)
registration.upgraded = true;
channel.writeAndFlush(registration);
return;
}
// remove the ConnectionWrapper (that was used to upgrade the connection)
cleanupPipeline(metaChannel);
workerEventLoop.schedule(new Runnable() {
@Override
public
void run() {
logger.trace("Notify Connection");
doConnect(metaChannel);
}
}, 20, TimeUnit.MILLISECONDS);
}
}

View File

@ -15,17 +15,21 @@
*/
package dorkbox.network.connection.registration.remote;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.EventLoopGroup;
public
class RegistrationRemoteHandlerClientTCP extends RegistrationRemoteHandlerClient {
public
RegistrationRemoteHandlerClientTCP(final String name, final RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationRemoteHandlerClientTCP(final String name,
final RegistrationWrapper registrationWrapper,
final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
}
/**
@ -76,7 +80,14 @@ class RegistrationRemoteHandlerClientTCP extends RegistrationRemoteHandlerClient
}
else {
logger.error("Error registering TCP with remote server!");
shutdown(channel, 0);
// this is what happens when the registration happens too quickly...
Object connection = context.pipeline().last();
if (connection instanceof ConnectionImpl) {
((ConnectionImpl) connection).channelRead(context, message);
} else {
shutdown(channel, 0);
}
}
}
}

View File

@ -18,18 +18,22 @@ package dorkbox.network.connection.registration.remote;
import java.io.IOException;
import java.net.InetSocketAddress;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.EventLoopGroup;
@SuppressWarnings("Duplicates")
public
class RegistrationRemoteHandlerClientUDP extends RegistrationRemoteHandlerClient {
public
RegistrationRemoteHandlerClientUDP(final String name, final RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationRemoteHandlerClientUDP(final String name,
final RegistrationWrapper registrationWrapper,
final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
}
/**
@ -53,12 +57,6 @@ class RegistrationRemoteHandlerClientUDP extends RegistrationRemoteHandlerClient
if (firstSession != null) {
outboundRegister.sessionID = firstSession.sessionId;
outboundRegister.hasMore = registrationWrapper.hasMoreRegistrations();
// when we have a "continuing registration" for another protocol, we have to have another roundtrip.
// outboundRegister.payload = new byte[0];
firstSession.updateRoundTripOnWrite();
}
// no size info, since this is UDP, it is not segmented
@ -102,7 +100,15 @@ class RegistrationRemoteHandlerClientUDP extends RegistrationRemoteHandlerClient
}
else {
logger.error("Error registering UDP with remote server!");
shutdown(channel, 0);
// this is what happens when the registration happens too quickly...
Object connection = context.pipeline()
.last();
if (connection instanceof ConnectionImpl) {
((ConnectionImpl) connection).channelRead(context, message);
}
else {
shutdown(channel, 0);
}
}
}
}

View File

@ -39,6 +39,8 @@ import dorkbox.network.connection.registration.Registration;
import dorkbox.util.crypto.CryptoECC;
import dorkbox.util.serialization.EccPublicKeySerializer;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.EventLoopGroup;
public
class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler {
@ -50,8 +52,8 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler {
private volatile long ecdhTimeout = System.nanoTime();
RegistrationRemoteHandlerServer(final String name, final RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationRemoteHandlerServer(final String name, final RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
}
/**
@ -83,12 +85,10 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler {
* UDP has a VERY limited size, so we have to break up registration steps into the following
* 1) session ID == 0 -> exchange session ID and public keys (session ID != 0 now)
* 2) session ID != 0 -> establish ECDH shared secret as AES key/iv
* 3)
*
*/
@SuppressWarnings("Duplicates")
void readServer(final Channel channel, final Registration registration, final String type, final MetaChannel metaChannel) {
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
void readServer(final ChannelHandlerContext context, final Channel channel, final Registration registration, final String type, final MetaChannel metaChannel) {
final InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
// IN: session ID == 0 (start of new connection)
// OUT: session ID + public key + ecc parameters (which are a nonce. the SERVER defines what these are)
@ -115,8 +115,8 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler {
outboundRegister.publicKey = registrationWrapper.getPublicKey();
outboundRegister.eccParameters = CryptoECC.generateSharedParameters(registrationWrapper.getSecureRandom());
metaChannel.updateRoundTripOnWrite();
channel.writeAndFlush(outboundRegister);
metaChannel.updateRoundTripOnWrite();
return;
}
@ -172,48 +172,71 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler {
EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic());
outboundRegister.payload = output.toBytes();
metaChannel.updateRoundTripOnWrite();
channel.writeAndFlush(outboundRegister);
metaChannel.updateRoundTripOnWrite();
return;
}
// ALWAYS upgrade the connection at this point.
// IN: upgraded=false if we haven't upgraded to encryption yet (this will always be the case right after encryption is setup)
// NOTE: if we have more registrations, we will "bounce back" that status so the client knows what to do.
// IN: hasMore=true if we have more registrations to do, false otherwise
if (!registration.upgraded) {
// upgrade the connection to an encrypted connection
registration.upgrade = true;
upgradeDecoders(channel, metaChannel);
// bounce back to the client so it knows we received it
channel.write(registration);
// this pipeline encoder/decoder can now be upgraded, and the "connection" added
upgradeEncoders(channel, metaChannel, remoteAddress);
if (!registration.hasMore) {
// we only get this when we are 100% done with the registration of all connection types.
// setup the pipeline with the real connection
upgradePipeline(metaChannel, remoteAddress);
}
channel.flush();
metaChannel.updateRoundTripOnWrite();
return;
}
// do we have any more registrations?
//
//
// we only get this when we are 100% done with the registration of all connection types.
// the context is the LAST protocol to be registered
//
//
// IN: hasMore=true if we have more registrations to do, false otherwise
if (!registration.hasMore) {
// // when we have a "continuing registration" for another protocol, we have to have another roundtrip.
// if (registration.payload != null) {
// metaChannel.updateRoundTripTime();
// channel.writeAndFlush(registration);
// return;
// }
// make sure we don't try to upgrade the client again.
registration.upgrade = false;
// have to get the delay before we update the round-trip time. We cannot have a delay that is BIGGER than our idle timeout!
final int idleTimeout = Math.max(2, this.registrationWrapper.getIdleTimeout()-2); // 2 because it is a reasonable amount for the "standard" delay amount
final long delay = Math.max(idleTimeout, TimeUnit.NANOSECONDS.toMillis(metaChannel.getRoundTripTime()));
logger.trace("Notify delay MS: {}", delay);
// we only get this when we are 100% done with the registration of all connection types.
// remove the ConnectionWrapper (that was used to upgrade the connection)
cleanupPipeline(metaChannel);
// have to get the delay before we update the round-trip time
final long delay = TimeUnit.NANOSECONDS.toMillis(metaChannel.getRoundTripTime() * 2);
// this tells the client we are ready to connect (we just bounce back the original message)
channel.writeAndFlush(registration);
// causes client to setup network connection & AES (we just bounce back the original message)
metaChannel.updateRoundTripOnWrite();
channel.writeAndFlush(registration);
setupConnectionCrypto(metaChannel, remoteAddress);
setupConnection(metaChannel, channel);
// wait for a "round trip" amount of time, then notify the APP!
channel.eventLoop()
.schedule(new Runnable() {
@Override
public
void run() {
logger.trace("Notify Connection");
notifyConnection(metaChannel);
}
}, delay, TimeUnit.MILLISECONDS);
}
// otherwise we have more registrations...
// wait for a "round trip" amount of time, then notify the APP!
workerEventLoop.schedule(new Runnable() {
@Override
public
void run() {
logger.trace("Notify Connection");
doConnect(metaChannel);
}
}, delay, TimeUnit.MILLISECONDS);
}
}

View File

@ -15,18 +15,22 @@
*/
package dorkbox.network.connection.registration.remote;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.EventLoopGroup;
public
class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer {
public
RegistrationRemoteHandlerServerTCP(final String name, final RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationRemoteHandlerServerTCP(final String name,
final RegistrationWrapper registrationWrapper,
final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
}
/**
@ -47,6 +51,7 @@ class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer
if (sessionId == 0) {
metaChannel = registrationWrapper.createSessionServer();
metaChannel.tcpChannel = channel;
// TODO: use this: channel.voidPromise();
logger.debug("New TCP connection. Saving meta-channel id: {}", metaChannel.sessionId);
}
else {
@ -59,11 +64,20 @@ class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer
}
}
readServer(channel, registration, "TCP server", metaChannel);
readServer(context, channel, registration, "TCP server", metaChannel);
}
else {
logger.error("Error registering TCP with remote client!");
shutdown(channel, 0);
// this is what happens when the registration happens too quickly...
Object connection = context.pipeline()
.last();
if (connection instanceof ConnectionImpl) {
((ConnectionImpl) connection).channelRead(context, message);
}
else {
shutdown(channel, 0);
}
}
}
}

View File

@ -15,20 +15,24 @@
*/
package dorkbox.network.connection.registration.remote;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.EventLoopGroup;
@SuppressWarnings("Duplicates")
@Sharable
public
class RegistrationRemoteHandlerServerUDP extends RegistrationRemoteHandlerServer {
public
RegistrationRemoteHandlerServerUDP(final String name, final RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
RegistrationRemoteHandlerServerUDP(final String name,
final RegistrationWrapper registrationWrapper,
final EventLoopGroup workerEventLoop) {
super(name, registrationWrapper, workerEventLoop);
}
@Override
@ -60,11 +64,20 @@ class RegistrationRemoteHandlerServerUDP extends RegistrationRemoteHandlerServer
metaChannel.udpChannel = channel;
}
readServer(channel, registration, "UDP server", metaChannel);
readServer(context, channel, registration, "UDP server", metaChannel);
}
else {
logger.error("Error registering UDP with remote client!");
shutdown(channel, 0);
// this is what happens when the registration happens too quickly...
Object connection = context.pipeline()
.last();
if (connection instanceof ConnectionImpl) {
((ConnectionImpl) connection).channelRead(context, message);
}
else {
shutdown(channel, 0);
}
}
}
}

View File

@ -42,6 +42,7 @@ class ChannelLocalWrapper implements ChannelWrapper, ConnectionPoint {
ChannelLocalWrapper(MetaChannel metaChannel, final RmiObjectHandler rmiObjectHandler) {
this.channel = metaChannel.localChannel;
this.rmiObjectHandler = rmiObjectHandler;
this.remoteAddress = ((LocalAddress) this.channel.remoteAddress()).id();
}
/**
@ -76,15 +77,6 @@ class ChannelLocalWrapper implements ChannelWrapper, ConnectionPoint {
return this;
}
/**
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
*/
@Override
public final
void init() {
this.remoteAddress = ((LocalAddress) this.channel.remoteAddress()).id();
}
/**
* Flushes the contents of the LOCAL pipes to the actual transport.
*/

View File

@ -55,6 +55,7 @@ class ChannelNetwork implements ConnectionPoint {
return channel.isWritable();
}
@Override
public
void flush() {
if (shouldFlush.compareAndSet(true, false)) {
@ -71,7 +72,9 @@ class ChannelNetwork implements ConnectionPoint {
public
void close(long maxShutdownWaitTimeInMilliSeconds) {
shouldFlush.set(false);
channel.close()
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
if (channel.isActive()) {
channel.close()
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
}
}
}

View File

@ -27,6 +27,7 @@ import dorkbox.network.connection.ISessionManager;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.rmi.RmiObjectHandler;
import dorkbox.util.FastThreadLocal;
import io.netty.bootstrap.DatagramCloseMessage;
import io.netty.util.NetUtil;
public
@ -107,15 +108,6 @@ class ChannelNetworkWrapper implements ChannelWrapper {
return this.udp;
}
/**
* 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.
}
/**
* Flushes the contents of the TCP/UDP/etc pipes to the actual transport.
*/
@ -170,6 +162,15 @@ class ChannelNetworkWrapper implements ChannelWrapper {
}
if (this.udp != null) {
// send a hint to the other connection that we should close. While not always 100% successful, this helps clean up connections
// on the remote end
try {
this.udp.write(new DatagramCloseMessage());
this.udp.flush();
Thread.yield();
} catch (Exception e) {
e.printStackTrace();
}
this.udp.close(maxShutdownWaitTimeInMilliSeconds);
}

View File

@ -33,20 +33,23 @@ class ChannelNull implements ConnectionPoint {
ChannelNull() {
}
/**
* Write an object to the underlying channel
*/
@Override
public
void write(Object object) {
}
@Override
public
void flush() {
}
/**
* @return true if the channel is writable. Useful when sending large amounts of data at once.
*/
@Override
public
boolean isWritable() {
// this channel is ALWAYS writable! (it just does nothing...)
return true;
}

View File

@ -28,11 +28,6 @@ interface ChannelWrapper {
ConnectionPoint tcp();
ConnectionPoint udp();
/**
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
*/
void init();
/**
* Flushes the contents of the TCP/UDP/etc pipes to the actual transport.
*/

View File

@ -0,0 +1,24 @@
/*
* Copyright 2018 dorkbox, llc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.pipeline;
/**
* What is the underlying connection type?
*/
public
enum ConnectionType {
LOCAL, OIO, NIO, EPOLL, KQUEUE
}

View File

@ -15,30 +15,20 @@
*/
package dorkbox.network.pipeline;
import io.netty.buffer.ByteBuf;
/**
*
* Magic bytes used to identify packets when they are specific types
*/
public
class MagicBytes {
/**
* bit masks
* <p>
* 0 means it's not encrypted or anything....
*/
public static final byte crypto = (byte) (1 << 1);
/**
* Determines if this buffer is encrypted or not.
*
* REGISTRATION is the ONLY thing NOT encrypted
* encrypted Y/N is always written by the serialization writer, so it is ALWAYS safe to check it here.
*/
public static
boolean isEncrypted(final ByteBuf buffer) {
// read off the magic byte
byte magicByte = buffer.getByte(buffer.readerIndex());
return (magicByte & crypto) == crypto;
}
// BROADCAST ...
public static final byte broadcastID = (byte) 42;
public static final byte broadcastResponseID = (byte) 57;
public static final byte HAS_TCP = (byte) (1 << 1);
public static final byte HAS_UDP = (byte) (1 << 2);
// max number of bytes in a broadcast packet (if both TCP and UDP are enabled)
public static final int maxPacketSize = 6;
// END BROADCAST ...
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2018 dorkbox, llc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.pipeline.discovery;
import java.net.InetAddress;
/**
* Holds the broadcast response data
*/
public
class BroadcastResponse {
public final InetAddress remoteAddress;
public final int tcpPort;
public final int udpPort;
BroadcastResponse(final InetAddress remoteAddress, final int tcpPort, final int udpPort) {
this.remoteAddress = remoteAddress;
this.tcpPort = tcpPort;
this.udpPort = udpPort;
}
}

View File

@ -15,11 +15,12 @@
*/
package dorkbox.network.pipeline.discovery;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import dorkbox.network.Broadcast;
import dorkbox.network.Server;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.pipeline.MagicBytes;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.socket.DatagramPacket;
@ -31,24 +32,77 @@ public
class BroadcastServer {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Server.class.getSimpleName());
private final int tcpPort;
private final int udpPort;
private final int bufferSize;
public
BroadcastServer() {
this.bufferSize = 0;
this.tcpPort = 0;
this.udpPort = 0;
}
public
BroadcastServer(final int tcpPort, final int udpPort) {
this.tcpPort = tcpPort;
this.udpPort = udpPort;
// either it will be TCP or UDP, or BOTH
if (tcpPort > 0 ^ udpPort > 0) {
// TCP or UDP
// ID + TCP or UDP ID + TCP or UDP port
bufferSize = 4;
}
else {
// BOTH
// ID + TCP and UDP ID + TCP and UDP port
bufferSize = 6;
}
}
/**
* @return true if the broadcast was responded to, false if it was not a broadcast (and there was no response)
*/
public boolean isBroadcast(final Channel channel, ByteBuf byteBuf, final InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
public boolean isDiscoveryRequest(final Channel channel, ByteBuf byteBuf, final InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
if (byteBuf.readableBytes() == 1) {
// this is a BROADCAST discovery event. Don't read the byte unless it is...
if (byteBuf.getByte(0) == Broadcast.broadcastID) {
if (byteBuf.getByte(0) == MagicBytes.broadcastID) {
byteBuf.readByte(); // read the byte to consume it (now that we verified it is a broadcast byte)
// absolutely MUST send packet > 0 across, otherwise netty will think it failed to write to the socket, and keep trying.
// (this bug was fixed by netty, however we are keeping this code)
ByteBuf directBuffer = channel.alloc()
.ioBuffer(1);
directBuffer.writeByte(Broadcast.broadcastResponseID);
.ioBuffer(bufferSize);
directBuffer.writeByte(MagicBytes.broadcastResponseID);
// now output the port information for TCP/UDP so the broadcast client knows which port to connect to
// either it will be TCP or UDP, or BOTH
int enabledFlag = 0;
if (tcpPort > 0) {
enabledFlag |= MagicBytes.HAS_TCP;
}
if (udpPort > 0) {
enabledFlag |= MagicBytes.HAS_UDP;
}
directBuffer.writeByte(enabledFlag);
// TCP is always first
if (tcpPort > 0) {
directBuffer.writeShort(tcpPort);
}
if (udpPort > 0) {
directBuffer.writeShort(udpPort);
}
channel.writeAndFlush(new DatagramPacket(directBuffer, remoteAddress, localAddress));
@ -61,4 +115,41 @@ class BroadcastServer {
return false;
}
/**
* @return true if this is a broadcast response, false if it was not a broadcast response
*/
public static
boolean isDiscoveryResponse(ByteBuf byteBuf, final InetAddress remoteAddress, final Channel channel) {
if (byteBuf.readableBytes() <= MagicBytes.maxPacketSize) {
// this is a BROADCAST discovery RESPONSE event. Don't read the byte unless it is...
if (byteBuf.getByte(0) == MagicBytes.broadcastResponseID) {
byteBuf.readByte(); // read the byte to consume it (now that we verified it is a broadcast byte)
// either it will be TCP or UDP, or BOTH
int typeID = byteBuf.readByte();
int tcpPort = 0;
int udpPort = 0;
// TCP is always first
if ((typeID & MagicBytes.HAS_TCP) == MagicBytes.HAS_TCP) {
tcpPort = byteBuf.readUnsignedShort();
}
if ((typeID & MagicBytes.HAS_UDP) == MagicBytes.HAS_UDP) {
udpPort = byteBuf.readUnsignedShort();
}
channel.attr(ClientDiscoverHostHandler.STATE)
.set(new BroadcastResponse(remoteAddress, tcpPort, udpPort));
byteBuf.release();
return true;
}
}
return false;
}
}

View File

@ -15,29 +15,33 @@
*/
package dorkbox.network.pipeline.discovery;
import dorkbox.network.Broadcast;
import java.net.InetAddress;
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;
public
class ClientDiscoverHostHandler extends SimpleChannelInboundHandler<DatagramPacket> {
// This uses CHANNEL LOCAL to save the data.
// This uses CHANNEL LOCAL DATA to save the data.
public static final AttributeKey<InetSocketAddress> STATE = AttributeKey.valueOf(ClientDiscoverHostHandler.class, "Discover.state");
public static final AttributeKey<BroadcastResponse> STATE = AttributeKey.valueOf(ClientDiscoverHostHandler.class, "Discover.state");
ClientDiscoverHostHandler() {
}
@Override
protected
void channelRead0(final ChannelHandlerContext context, final DatagramPacket message) throws Exception {
ByteBuf data = message.content();
if (data.readableBytes() == 1 && data.readByte() == Broadcast.broadcastResponseID) {
context.channel()
.attr(STATE)
.set(message.sender());
ByteBuf byteBuf = message.content();
InetAddress remoteAddress = message.sender()
.getAddress();
if (BroadcastServer.isDiscoveryResponse(byteBuf, remoteAddress, context.channel())) {
// the state/ports/etc are set inside the isDiscoveryResponse() method...
context.channel()
.close();
}

View File

@ -36,7 +36,7 @@ class KryoDecoder extends ByteToMessageDecoder {
@SuppressWarnings("unused")
protected
Object readObject(CryptoSerializationManager serializationManager, ChannelHandlerContext context, ByteBuf in, int length) throws IOException {
Object readObject(CryptoSerializationManager serializationManager, ChannelHandlerContext context, ByteBuf in, int length) throws Exception {
// no connection here because we haven't created one yet. When we do, we replace this handler with a new one.
return serializationManager.read(in, length);
}
@ -167,7 +167,7 @@ class KryoDecoder extends ByteToMessageDecoder {
object = readObject(serializationManager, context, in, length);
out.add(object);
} catch (Exception ex) {
context.fireExceptionCaught(new IOException("Unable to deserialize object!", ex));
context.fireExceptionCaught(new IOException("Unable to deserialize object for " + this.getClass(), ex));
}
}
}

View File

@ -15,9 +15,7 @@
*/
package dorkbox.network.pipeline.tcp;
import java.io.IOException;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.CryptoConnection;
import dorkbox.network.serialization.CryptoSerializationManager;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
@ -37,10 +35,14 @@ class KryoDecoderCrypto extends KryoDecoder {
Object readObject(final CryptoSerializationManager serializationManager,
final ChannelHandlerContext context,
final ByteBuf in,
final int length) throws IOException {
final int length) throws Exception {
ConnectionImpl connection = (ConnectionImpl) context.pipeline()
.last();
return serializationManager.readWithCrypto(connection, in, length);
try {
CryptoConnection connection = (CryptoConnection) context.pipeline()
.last();
return serializationManager.readWithCrypto(connection, in, length);
} catch (Exception e) {
throw e;
}
}
}

View File

@ -65,7 +65,7 @@ class KryoEncoder extends MessageToByteEncoder<Object> {
int index = out.writerIndex();
// now set the frame length
// (reservedLengthLength) 5 is the reserved space for the integer.
// (reservedLengthLength) 4 is the reserved space for the integer.
int length = index - startIndex;
// specify the header.

View File

@ -17,7 +17,7 @@ package dorkbox.network.pipeline.tcp;
import java.io.IOException;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.CryptoConnection;
import dorkbox.network.serialization.CryptoSerializationManager;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
@ -39,8 +39,8 @@ class KryoEncoderCrypto extends KryoEncoder {
final Object msg,
final ByteBuf buffer) throws IOException {
ConnectionImpl connection = (ConnectionImpl) context.pipeline()
.last();
CryptoConnection connection = (CryptoConnection) context.pipeline()
.last();
serializationManager.writeWithCrypto(connection, buffer, msg);
}
}

View File

@ -20,10 +20,9 @@ import java.util.List;
import org.slf4j.LoggerFactory;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.CryptoConnection;
import dorkbox.network.serialization.CryptoSerializationManager;
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;
@ -43,12 +42,12 @@ class KryoDecoderUdpCrypto extends MessageToMessageDecoder<DatagramPacket> {
@Override
public
void decode(ChannelHandlerContext context, DatagramPacket in, List<Object> out) throws Exception {
ChannelHandler last = context.pipeline()
.last();
CryptoConnection last = (CryptoConnection) context.pipeline()
.last();
try {
ByteBuf data = in.content();
Object object = serializationManager.readWithCrypto((ConnectionImpl) last, data, data.readableBytes());
Object object = serializationManager.readWithCrypto(last, data, data.readableBytes());
out.add(object);
} catch (IOException e) {
String message = "Unable to deserialize object";

View File

@ -17,10 +17,9 @@ package dorkbox.network.pipeline.udp;
import java.io.IOException;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.CryptoConnection;
import dorkbox.network.serialization.CryptoSerializationManager;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
@ -37,8 +36,8 @@ class KryoEncoderUdpCrypto extends KryoEncoderUdp {
void writeObject(CryptoSerializationManager serializationManager, ChannelHandlerContext ctx, Object msg, ByteBuf buffer)
throws IOException {
ChannelHandler last = ctx.pipeline()
.last();
serializationManager.writeWithCrypto((ConnectionImpl) last, buffer, msg);
CryptoConnection last = (CryptoConnection) ctx.pipeline()
.last();
serializationManager.writeWithCrypto(last, buffer, msg);
}
}

View File

@ -293,8 +293,7 @@ class RmiBridge {
}
// System.err.println("Sending: " + invokeMethod.responseID);
connection.send()
.TCP(invokeMethodResult);
connection.send(invokeMethodResult).flush();
// logger.error("{} sent data: {} with id ({})", connection, result, invokeMethod.responseID);
}

View File

@ -149,7 +149,8 @@ class RmiObjectLocalHandler extends RmiObjectHandler {
Class<?> rmiImpl = serialization.getRmiImpl(registration.interfaceClass);
RmiRegistration registrationResult = connection.createNewRmiObject(interfaceClass, rmiImpl, callbackId);
connection.TCP(registrationResult);
connection.send(registrationResult);
// connection transport is flushed in calling method (don't need to do it here)
}
// Check if we are getting an already existing REMOTE object. This check is always AFTER the check to create a new object
@ -158,8 +159,8 @@ class RmiObjectLocalHandler extends RmiObjectHandler {
//
// GET a LOCAL rmi object, if none get a specific, GLOBAL rmi object (objects that are not bound to a single connection).
RmiRegistration registrationResult = connection.getExistingRmiObject(interfaceClass, registration.rmiId, callbackId);
connection.TCP(registrationResult);
connection.send(registrationResult);
// connection transport is flushed in calling method (don't need to do it here)
}
}
else {

View File

@ -56,7 +56,8 @@ class RmiObjectNetworkHandler extends RmiObjectHandler {
// For network connections, the interface class kryo ID == implementation class kryo ID, so they switch automatically.
RmiRegistration registrationResult = connection.createNewRmiObject(interfaceClass, interfaceClass, callbackId);
connection.TCP(registrationResult);
connection.send(registrationResult);
// connection transport is flushed in calling method (don't need to do it here)
}
// Check if we are getting an already existing REMOTE object. This check is always AFTER the check to create a new object
@ -65,7 +66,8 @@ class RmiObjectNetworkHandler extends RmiObjectHandler {
//
// GET a LOCAL rmi object, if none get a specific, GLOBAL rmi object (objects that are not bound to a single connection).
RmiRegistration registrationResult = connection.getExistingRmiObject(interfaceClass, registration.rmiId, callbackId);
connection.TCP(registrationResult);
connection.send(registrationResult);
// connection transport is flushed in calling method (don't need to do it here)
}
}
else {

View File

@ -46,7 +46,11 @@ import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dorkbox.network.connection.*;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.KryoExtra;
import dorkbox.network.connection.Listener;
import dorkbox.network.serialization.RmiSerializationManager;
/**
@ -275,10 +279,12 @@ class RmiProxyHandler implements InvocationHandler {
// Sends our invokeMethod to the remote connection, which the RmiBridge listens for
if (this.udp) {
this.connection.UDP(invokeMethod);
// flush is necessary in case this is called outside of a network worker thread
this.connection.UDP(invokeMethod).flush();
}
else {
this.connection.TCP(invokeMethod);
// flush is necessary in case this is called outside of a network worker thread
this.connection.send(invokeMethod).flush();
}
if (logger.isTraceEnabled()) {

View File

@ -99,6 +99,15 @@ class RmiUtils {
}
/**
* @param logger
* @param kryo
* @param asmEnabled
* @param iFace this is never null.
* @param impl this is NULL on the rmi "client" side. This is NOT NULL on the "server" side (where the object lives)
* @param classId
* @return
*/
public static
CachedMethod[] getCachedMethods(final Logger logger, final Kryo kryo, final boolean asmEnabled, final Class<?> iFace, final Class<?> impl, final int classId) {
MethodAccess ifaceMethodAccess = null;
@ -145,28 +154,30 @@ class RmiUtils {
// copy because they can be overridden
boolean overriddenMethod = false;
Method tweakMethod = method;
MethodAccess tweakMethodAccess = ifaceMethodAccess;
// this is how we detect if the method has been changed from the interface -> implementation + connection parameter
if (declaringClass.equals(impl)) {
tweakMethodAccess = implMethodAccess;
overriddenMethod = true;
if (logger.isTraceEnabled())
logger.trace("Overridden method: {}.{}", impl, method.getName());
if (logger.isTraceEnabled()) {
logger.trace("Overridden method: {}.{}", impl, method.getName());
}
}
CachedMethod cachedMethod = null;
if (tweakMethodAccess != null) {
// reflectAsm doesn't like "Object" class methods...
if (tweakMethodAccess != null && method.getDeclaringClass() != Object.class) {
try {
final int index = tweakMethodAccess.getIndex(tweakMethod.getName(), parameterTypes);
final int index = tweakMethodAccess.getIndex(method.getName(), parameterTypes);
AsmCachedMethod asmCachedMethod = new AsmCachedMethod();
asmCachedMethod.methodAccessIndex = index;
asmCachedMethod.methodAccess = tweakMethodAccess;
asmCachedMethod.name = tweakMethod.getName();
asmCachedMethod.name = method.getName();
if (overriddenMethod) {
// logger.error(tweakMethod.getName() + " " + Arrays.toString(parameterTypes) + " index: " + index +
@ -188,7 +199,7 @@ class RmiUtils {
cachedMethod = asmCachedMethod;
} catch (Exception e) {
logger.trace("Unable to use ReflectAsm for {}.{}", declaringClass, tweakMethod.getName(), e);
logger.trace("Unable to use ReflectAsm for {}.{} (using java reflection instead)", declaringClass, method.getName(), e);
}
}
@ -200,7 +211,7 @@ class RmiUtils {
cachedMethod.methodClassID = classId;
// we ALSO have to setup "normal" reflection access to these methods
cachedMethod.method = tweakMethod;
cachedMethod.method = method;
cachedMethod.methodIndex = i;
// Store the serializer for each final parameter.
@ -363,5 +374,4 @@ class RmiUtils {
return methodsArray;
}
}

View File

@ -17,7 +17,7 @@ package dorkbox.network.serialization;
import java.io.IOException;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.CryptoConnection;
import io.netty.buffer.ByteBuf;
/**
@ -25,14 +25,14 @@ import io.netty.buffer.ByteBuf;
* defeats the point of multi-threaded
*/
public
interface CryptoSerializationManager extends RmiSerializationManager {
interface CryptoSerializationManager<C extends CryptoConnection> extends RmiSerializationManager {
/**
* Waits until a kryo is available to write, using CAS operations to prevent having to synchronize.
* <p/>
* There is a small speed penalty if there were no kryo's available to use.
*/
void writeWithCrypto(ConnectionImpl connection, ByteBuf buffer, Object message) throws IOException;
void writeWithCrypto(C connection, ByteBuf buffer, Object message) throws IOException;
/**
* Reads an object from the buffer.
@ -44,5 +44,5 @@ interface CryptoSerializationManager extends RmiSerializationManager {
* @param length
* should ALWAYS be the length of the expected object!
*/
Object readWithCrypto(ConnectionImpl connection, ByteBuf buffer, int length) throws IOException;
Object readWithCrypto(C connection, ByteBuf buffer, int length) throws IOException;
}

View File

@ -43,7 +43,7 @@ import com.esotericsoftware.kryo.util.IntMap;
import com.esotericsoftware.kryo.util.MapReferenceResolver;
import com.esotericsoftware.kryo.util.Util;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.CryptoConnection;
import dorkbox.network.connection.KryoExtra;
import dorkbox.network.connection.ping.PingMessage;
import dorkbox.network.rmi.CachedMethod;
@ -64,6 +64,7 @@ import dorkbox.util.serialization.EccPublicKeySerializer;
import dorkbox.util.serialization.IesParametersSerializer;
import dorkbox.util.serialization.IesWithCipherParametersSerializer;
import dorkbox.util.serialization.UnmodifiableCollectionsSerializer;
import io.netty.bootstrap.DatagramCloseMessage;
import io.netty.buffer.ByteBuf;
/**
@ -75,7 +76,7 @@ import io.netty.buffer.ByteBuf;
*/
@SuppressWarnings({"unused", "StaticNonFinalField"})
public
class Serialization implements CryptoSerializationManager, RmiSerializationManager {
class Serialization<C extends CryptoConnection> implements CryptoSerializationManager<C>, RmiSerializationManager {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Serialization.class.getSimpleName());
@ -182,17 +183,18 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
* Kryo#newDefaultSerializer(Class)
*/
public static
Serialization DEFAULT(final boolean references,
<C extends CryptoConnection> Serialization<C> DEFAULT(final boolean references,
final boolean registrationRequired,
final boolean implementationRequired,
final SerializerFactory factory) {
final Serialization serialization = new Serialization(references,
final Serialization<C> serialization = new Serialization<C>(references,
registrationRequired,
implementationRequired,
factory);
serialization.register(PingMessage.class);
serialization.register(DatagramCloseMessage.class);
serialization.register(byte[].class);
serialization.register(IESParameters.class, new IesParametersSerializer());
@ -241,7 +243,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
}
}
private boolean initialized = false;
private final ObjectPool<KryoExtra> kryoPool;
private final ObjectPool<KryoExtra<C>> kryoPool;
// used to determine if we should forbid interface registration OUTSIDE of RMI registration.
private final boolean forbidInterfaceRegistration;
@ -311,13 +313,13 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
this.forbidInterfaceRegistration = implementationRequired;
this.kryoPool = ObjectPool.NonBlockingSoftReference(new PoolableObject<KryoExtra>() {
this.kryoPool = ObjectPool.NonBlockingSoftReference(new PoolableObject<KryoExtra<C>>() {
@Override
public
KryoExtra create() {
KryoExtra<C> create() {
synchronized (Serialization.this) {
// we HAVE to pre-allocate the KRYOs
KryoExtra kryo = new KryoExtra(Serialization.this);
KryoExtra<C> kryo = new KryoExtra<C>(Serialization.this);
kryo.getFieldSerializerConfig()
.setUseAsm(useAsm);
@ -662,7 +664,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
@Override
public final
void write(final ByteBuf buffer, final Object message) throws IOException {
final KryoExtra kryo = kryoPool.take();
final KryoExtra<C> kryo = kryoPool.take();
try {
kryo.write(buffer, message);
} finally {
@ -680,7 +682,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
@Override
public final
Object read(final ByteBuf buffer, final int length) throws IOException {
final KryoExtra kryo = kryoPool.take();
final KryoExtra<C> kryo = kryoPool.take();
try {
return kryo.read(buffer);
} finally {
@ -694,7 +696,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
@Override
public
void writeFullClassAndObject(final Logger logger, final Output output, final Object value) throws IOException {
KryoExtra kryo = kryoPool.take();
KryoExtra<C> kryo = kryoPool.take();
boolean prev = false;
try {
@ -720,7 +722,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
@Override
public
Object readFullClassAndObject(final Logger logger, final Input input) throws IOException {
KryoExtra kryo = kryoPool.take();
KryoExtra<C> kryo = kryoPool.take();
boolean prev = false;
try {
@ -751,9 +753,9 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
// initialize the kryo pool with at least 1 kryo instance. This ALSO makes sure that all of our class registration is done
// correctly and (if not) we are are notified on the initial thread (instead of on the network update thread)
KryoExtra kryo = null;
KryoExtra<C> kryo = null;
try {
kryo = takeKryo();
kryo = kryoPool.take();
ClassResolver classResolver = kryo.getClassResolver();
@ -819,8 +821,8 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
*/
@Override
public final
void writeWithCrypto(final ConnectionImpl connection, final ByteBuf buffer, final Object message) throws IOException {
final KryoExtra kryo = kryoPool.take();
void writeWithCrypto(final C connection, final ByteBuf buffer, final Object message) throws IOException {
final KryoExtra<C> kryo = kryoPool.take();
try {
// we only need to encrypt when NOT on loopback, since encrypting on loopback is a waste of CPU
if (connection.isLoopback()) {
@ -845,8 +847,8 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag
@SuppressWarnings("Duplicates")
@Override
public final
Object readWithCrypto(final ConnectionImpl connection, final ByteBuf buffer, final int length) throws IOException {
final KryoExtra kryo = kryoPool.take();
Object readWithCrypto(final C connection, final ByteBuf buffer, final int length) throws IOException {
final KryoExtra<C> kryo = kryoPool.take();
try {
// we only need to encrypt when NOT on loopback, since encrypting on loopback is a waste of CPU
if (connection.isLoopback()) {

View File

@ -0,0 +1,22 @@
/*
* Copyright 2018 dorkbox, llc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.netty.bootstrap;
/**
*
*/
public
class DatagramCloseMessage {}

View File

@ -59,11 +59,18 @@ class SessionBootstrap extends AbstractBootstrap<SessionBootstrap, Channel> {
private final SessionBootstrapConfig config = new SessionBootstrapConfig(this);
private volatile EventLoopGroup childGroup;
private volatile ChannelHandler childHandler;
@SuppressWarnings("unchecked")
private volatile AddressResolverGroup<SocketAddress> resolver = (AddressResolverGroup<SocketAddress>) DEFAULT_RESOLVER;
private final int tcpPort;
private final int udpPort;
public
SessionBootstrap() { }
SessionBootstrap(final int tcpPort, final int udpPort) {
this.tcpPort = tcpPort;
this.udpPort = udpPort;
}
private
SessionBootstrap(SessionBootstrap bootstrap) {
@ -80,6 +87,9 @@ class SessionBootstrap extends AbstractBootstrap<SessionBootstrap, Channel> {
synchronized (bootstrap.childAttrs) {
childAttrs.putAll(bootstrap.childAttrs);
}
this.tcpPort = bootstrap.tcpPort;
this.udpPort = bootstrap.udpPort;
}
/**
@ -251,8 +261,8 @@ class SessionBootstrap extends AbstractBootstrap<SessionBootstrap, Channel> {
@Override
public
void run() {
pipeline.addLast(new SessionManager(ch,
currentChildGroup,
pipeline.addLast(new SessionManager(tcpPort, udpPort,
ch, currentChildGroup,
currentChildHandler,
currentChildOptions,
currentChildAttrs));

View File

@ -76,7 +76,7 @@ class SessionManager extends ChannelInboundHandlerAdapter {
return Long_.from(combined);
}
private final BroadcastServer broadcastServer = new BroadcastServer();
private final BroadcastServer broadcastServer;
// Does not need to be thread safe, because access only happens in the event loop
private final LongObjectHashMap<DatagramSessionChannel> datagramChannels = new LongObjectHashMap<DatagramSessionChannel>();
@ -91,7 +91,8 @@ class SessionManager extends ChannelInboundHandlerAdapter {
private final DatagramSessionChannelConfig sessionConfig;
SessionManager(final Channel channel,
SessionManager(final int tcpPort, final int udpPort,
final Channel channel,
EventLoopGroup childGroup,
ChannelHandler childHandler,
Entry<ChannelOption<?>, Object>[] childOptions,
@ -117,6 +118,8 @@ class SessionManager extends ChannelInboundHandlerAdapter {
.setAutoRead(true);
}
};
broadcastServer = new BroadcastServer(tcpPort, udpPort);
}
@Override
@ -148,7 +151,7 @@ class SessionManager extends ChannelInboundHandlerAdapter {
InetSocketAddress remoteAddress = packet.sender();
// check to see if it's a broadcast packet or not
if (broadcastServer.isBroadcast(channel, content, localAddress, remoteAddress)) {
if (broadcastServer.isDiscoveryRequest(channel, content, localAddress, remoteAddress)) {
// don't bother creating channels if this is a broadcast event. Just respond and be finished
return;
}

View File

@ -43,7 +43,7 @@ import io.netty.util.ResourceLeakDetector;
public abstract
class BaseTest {
public static final String host = "localhost";
public static final String host = "127.0.0.1";
public static final int tcpPort = 54558;
public static final int udpPort = 54779;
@ -75,8 +75,9 @@ class BaseTest {
// rootLogger.setLevel(Level.OFF);
rootLogger.setLevel(Level.DEBUG);
// rootLogger.setLevel(Level.TRACE);
// rootLogger.setLevel(Level.INFO);
// rootLogger.setLevel(Level.DEBUG);
rootLogger.setLevel(Level.TRACE);
// rootLogger.setLevel(Level.ALL);
@ -116,25 +117,19 @@ class BaseTest {
*/
public
void stopEndPoints() {
stopEndPoints(1);
stopEndPoints(0);
}
public
void stopEndPoints(final int stopAfterMillis) {
final String name = Thread.currentThread().getThreadGroup()
.getName();
ThreadGroup threadGroup = Thread.currentThread()
.getThreadGroup();
final String name = threadGroup.getName();
// no need to run inside another thread if we are not inside the client/server thread
if (!name.contains(THREADGROUP_NAME)) {
stopEndPoints_outsideThread();
return;
}
if (stopAfterMillis > 0) {
if (name.contains(THREADGROUP_NAME)) {
// We have to ALWAYS run this in a new thread, BECAUSE if stopEndPoints() is called from a client/server thread, it will
// DEADLOCK
final Thread thread = new Thread(getThreadGroup(), new Runnable() {
final Thread thread = new Thread(threadGroup.getParent(), new Runnable() {
@Override
public
void run() {
@ -143,106 +138,98 @@ class BaseTest {
// ARE NOT in the same thread group as netty!
Thread.sleep(stopAfterMillis);
stopEndPoints_outsideThread();
} catch (InterruptedException e) {
e.printStackTrace();
stopEndPoints(stopAfterMillis);
} catch (InterruptedException ignored) {
}
}
}, "UnitTest shutdown");
thread.setDaemon(true);
thread.start();
}
}
} else {
synchronized (this.endPointConnections) {
for (EndPoint endPointConnection : this.endPointConnections) {
endPointConnection.stop();
endPointConnection.waitForShutdown();
}
private
void stopEndPoints_outsideThread() {
synchronized (BaseTest.this.endPointConnections) {
for (EndPoint endPointConnection : BaseTest.this.endPointConnections) {
endPointConnection.stop();
endPointConnection.waitForShutdown();
this.endPointConnections.clear();
this.endPointConnections.notifyAll();
}
BaseTest.this.endPointConnections.clear();
}
}
private
ThreadGroup getThreadGroup() {
ThreadGroup threadGroup = Thread.currentThread()
.getThreadGroup();
final String name = threadGroup.getName();
if (name.contains(THREADGROUP_NAME)) {
threadGroup = threadGroup.getParent();
}
return threadGroup;
}
public
void waitForThreads(int stopAfterSecondsOrMillis) {
if (stopAfterSecondsOrMillis < 1000) {
stopAfterSecondsOrMillis *= 1000;
}
stopEndPoints(stopAfterSecondsOrMillis);
waitForThreads0(stopAfterSecondsOrMillis);
}
/**
* Wait for threads until they are done (no timeout)
* Wait for network client/server threads to shutdown on their own, BUT WILL ERROR (+ shutdown) if they take longer than 2 minutes.
*/
public
void waitForThreads() {
waitForThreads0(0);
waitForThreads(0);
}
private
void waitForThreads0(final int stopAfterMillis) {
/**
* Wait for network client/server threads to shutdown for the specified time.
*
* @param stopAfterSeconds how many seconds to wait
*/
public
void waitForThreads(int stopAfterSeconds) {
final int stopAfterMillis = stopAfterSeconds * 1000; // this must be in milliseconds
this.fail_check = false;
Thread thread = null;
synchronized (this.endPointConnections) {
Thread thread = null;
if (!this.endPointConnections.isEmpty()) {
// make sure to run this thread in the MAIN thread group..
ThreadGroup threadGroup = Thread.currentThread()
.getThreadGroup();
if (threadGroup.getName()
.contains(THREADGROUP_NAME)) {
threadGroup = threadGroup.getParent();
}
if (stopAfterMillis > 0L) {
stopEndPoints(stopAfterMillis);
// We have to ALWAYS run this in a new thread, BECAUSE if stopEndPoints() is called from a client/server thread, it will
// DEADLOCK
thread = new Thread(getThreadGroup(), new Runnable() {
@Override
public
void run() {
try {
thread = new Thread(threadGroup, new Runnable() {
@Override
public
void run() {
// not the best, but this works for our purposes. This is a TAD hacky, because we ALSO have to make sure that we
// ARE NOT in the same thread group as netty!
Thread.sleep(stopAfterMillis + 120000L); // test must run in 2 minutes or it fails
try {
if (stopAfterMillis > 0L) {
// if we specify a time, then we stop, otherwise we wait the timeout.
Thread.sleep(stopAfterMillis);
}
else {
Thread.sleep(120 * 1000L); // wait minimum of 2 minutes before we automatically fail the unit test.
}
BaseTest.this.fail_check = true;
} catch (InterruptedException ignored) {
System.err.println("Test did not complete in a timely manner...");
BaseTest.this.fail_check = true;
stopEndPoints();
} catch (InterruptedException ignored) {
}
}
}
}, "UnitTest timeout");
}, "UnitTest timeout fail condition");
thread.setDaemon(true);
thread.start();
}
thread.setDaemon(true);
thread.start();
}
while (true) {
synchronized (this.endPointConnections) {
if (this.endPointConnections.isEmpty()) {
break;
while (!this.endPointConnections.isEmpty()) {
try {
this.endPointConnections.wait(stopAfterMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
if (thread != null) {
thread.interrupt();
}
}
if (this.fail_check) {
fail("Test did not complete in a timely manner.");
}
if (thread != null) {
thread.interrupt();
if (this.fail_check) {
fail("Test did not complete in a timely manner.");
}
}
// Give sockets a chance to close before starting the next test.

View File

@ -63,13 +63,32 @@ public class ChunkedDataIdleTest extends BaseTest {
// have to test sending objects
@Test
public void SendUdp() throws SecurityException, IOException {
public
void SendUdp() throws SecurityException, IOException {
final Data mainData = new Data();
populateData(mainData);
System.err.println("-- UDP");
Configuration configuration = new Configuration();
// configuration.tcpPort = tcpPort;
configuration.udpPort = udpPort;
configuration.host = host;
configuration.serialization = Serialization.DEFAULT();
register(configuration.serialization);
sendObject(mainData, configuration, ConnectionType.UDP);
}
// have to test sending objects
@Test
public void SendTcpUdp_Udp() throws SecurityException, IOException {
final Data mainData = new Data();
populateData(mainData);
System.err.println("-- UDP (with TCP connection alive)");
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
configuration.udpPort = udpPort;
configuration.host = host;
@ -81,7 +100,7 @@ public class ChunkedDataIdleTest extends BaseTest {
// have to test sending objects
@Test
public void SendTcpAndUdp() throws SecurityException, IOException {
public void SendTcpUdp_Tcp() throws SecurityException, IOException {
final Data mainData = new Data();
populateData(mainData);
@ -95,7 +114,7 @@ public class ChunkedDataIdleTest extends BaseTest {
configuration.serialization = Serialization.DEFAULT();
register(configuration.serialization);
sendObject(mainData, configuration, ConnectionType.UDP);
sendObject(mainData, configuration, ConnectionType.TCP);
}

View File

@ -28,19 +28,21 @@ import org.junit.Test;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Listener;
import dorkbox.network.connection.Listener.OnConnected;
import dorkbox.network.connection.Listener.OnDisconnected;
import dorkbox.network.serialization.Serialization;
import dorkbox.util.exceptions.SecurityException;
import dorkbox.util.serialization.SerializationManager;
public
class ConnectionTest extends BaseTest {
private AtomicInteger succesCount;
private AtomicInteger successCount;
@Test
public
void connectLocal() throws SecurityException, IOException {
System.out.println("---- " + "Local");
succesCount = new AtomicInteger(0);
successCount = new AtomicInteger(0);
Configuration configuration = new Configuration();
configuration.localChannelName = EndPoint.LOCAL_CHANNEL;
@ -51,14 +53,14 @@ class ConnectionTest extends BaseTest {
startClient(configuration);
waitForThreads(10);
Assert.assertEquals(3, succesCount.get());
Assert.assertEquals(6, successCount.get());
}
@Test
public
void connectTcp() throws SecurityException, IOException {
System.out.println("---- " + "TCP");
succesCount = new AtomicInteger(0);
successCount = new AtomicInteger(0);
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
@ -71,14 +73,14 @@ class ConnectionTest extends BaseTest {
startClient(configuration);
waitForThreads(10);
Assert.assertEquals(3, succesCount.get());
Assert.assertEquals(6, successCount.get());
}
@Test
public
void connectUdp() throws SecurityException, IOException {
System.out.println("---- " + "UDP");
succesCount = new AtomicInteger(0);
successCount = new AtomicInteger(0);
Configuration configuration = new Configuration();
configuration.udpPort = udpPort;
@ -91,14 +93,14 @@ class ConnectionTest extends BaseTest {
startClient(configuration);
waitForThreads(10);
Assert.assertEquals(3, succesCount.get());
Assert.assertEquals(6, successCount.get());
}
@Test
public
void connectTcpUdp() throws SecurityException, IOException {
System.out.println("---- " + "TCP UDP");
succesCount = new AtomicInteger(0);
successCount = new AtomicInteger(0);
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
@ -112,35 +114,45 @@ class ConnectionTest extends BaseTest {
startClient(configuration);
waitForThreads(10);
Assert.assertEquals(3, succesCount.get());
Assert.assertEquals(6, successCount.get());
}
private
Server startServer(Configuration configuration) throws SecurityException {
Server server = new Server(configuration);
Server startServer(final Configuration configuration) throws SecurityException {
final Server server = new Server(configuration);
addEndPoint(server);
server.bind(false);
server.listeners()
.add(new Listener.OnConnected<Connection>() {
.add(new OnConnected<Connection>() {
@Override
public
void connected(final Connection connection) {
succesCount.getAndIncrement();
successCount.getAndIncrement();
}
});
server.listeners()
})
.add(new OnDisconnected<Connection>() {
@Override
public
void disconnected(Connection connection) {
successCount.getAndIncrement();
}
})
.add(new Listener.OnMessageReceived<Connection, Object>() {
@Override
public void received(Connection connection, Object message) {
System.err.println("Received message from client: " + message.getClass().getSimpleName());
succesCount.getAndIncrement();
connection.send()
.UDP(message);
connection.close();
successCount.getAndIncrement();
if (configuration.tcpPort > 0) {
connection.send()
.TCP(message);
}
else {
connection.send()
.UDP(message);
}
}
});
@ -148,7 +160,7 @@ class ConnectionTest extends BaseTest {
}
private
Client startClient(Configuration configuration) throws SecurityException, IOException {
Client startClient(final Configuration configuration) throws SecurityException, IOException {
Client client;
if (configuration != null) {
client = new Client(configuration);
@ -159,12 +171,18 @@ class ConnectionTest extends BaseTest {
addEndPoint(client);
client.listeners()
.add(new Listener.OnDisconnected<Connection>() {
.add(new OnConnected<Connection>() {
@Override
public
void connected(final Connection connection) {
successCount.getAndIncrement();
}
})
.add(new OnDisconnected<Connection>() {
@Override
public
void disconnected(Connection connection) {
succesCount.getAndIncrement();
stopEndPoints();
successCount.getAndIncrement();
}
})
.add(new Listener.OnMessageReceived<Connection, Object>() {
@ -173,18 +191,23 @@ class ConnectionTest extends BaseTest {
void received(Connection connection, Object message) {
System.err.println("Received message from server: " + message.getClass()
.getSimpleName());
System.out.println("Now disconnecting!");
succesCount.getAndIncrement();
connection.close();
System.err.println("Now disconnecting!");
successCount.getAndIncrement();
stopEndPoints();
}
});
client.connect(5000);
client.send()
.UDP(new BMessage());
if (true) {
throw new RuntimeException("wreha?");
if (configuration.tcpPort > 0) {
client.send()
.TCP(new BMessage());
}
else {
client.send()
.UDP(new BMessage());
}
return client;
}

View File

@ -30,11 +30,11 @@ import dorkbox.util.exceptions.SecurityException;
public
class DisconnectReconnectTest extends BaseTest {
private final Timer timer = new Timer();
@Test
public
void reconnect() throws SecurityException, IOException {
final Timer timer = new Timer();
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
@ -51,11 +51,12 @@ class DisconnectReconnectTest extends BaseTest {
@Override
public
void connected(final Connection connection) {
System.out.println("Disconnecting after 2 seconds.");
timer.schedule(new TimerTask() {
@Override
public
void run() {
System.out.println("Disconnecting after 2 seconds.");
System.out.println("Disconnecting....");
connection.close();
}
}, 2000);
@ -72,24 +73,26 @@ class DisconnectReconnectTest extends BaseTest {
@Override
public
void disconnected(Connection connection) {
if (reconnectCount.getAndIncrement() == 2) {
int count = reconnectCount.getAndIncrement();
if (count == 3) {
System.out.println("Shutting down");
stopEndPoints();
return;
}
System.out.println("Reconnecting: " + reconnectCount.get());
try {
client.reconnect();
} catch (IOException e) {
e.printStackTrace();
else {
System.out.println("Reconnecting: " + count);
try {
client.reconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
client.connect(5000);
waitForThreads();
System.err.println("Connection count (after reconnecting) is: " + reconnectCount.get());
assertEquals(3, reconnectCount.get());
assertEquals(4, reconnectCount.get());
}
}

View File

@ -27,6 +27,7 @@ import org.junit.Test;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
import dorkbox.network.pipeline.discovery.BroadcastResponse;
import dorkbox.util.exceptions.SecurityException;
public
@ -48,7 +49,7 @@ class DiscoverHostTest extends BaseTest {
// ----
String host = Broadcast.discoverHost(udpPort, 2000);
BroadcastResponse host = Broadcast.discoverHost(udpPort, 2000);
if (host == null) {
stopEndPoints();
fail("No servers found. Maybe you are behind a VPN service or your network is mis-configured?");
@ -77,7 +78,7 @@ class DiscoverHostTest extends BaseTest {
});
client.connect(2000);
waitForThreads(2);
waitForThreads(20);
if (!this.connected) {
fail("Unable to connect to server.");

View File

@ -39,14 +39,19 @@ import dorkbox.util.exceptions.SecurityException;
public
class MultipleThreadTest extends BaseTest {
private final Object lock = new Object();
private volatile boolean stillRunning = false;
private final Object finalRunLock = new Object();
private volatile boolean finalStillRunning = false;
private final int messageCount = 150;
private final int threadCount = 15;
private final int clientCount = 13;
private final List<Client> clients = new ArrayList<Client>(this.clientCount);
int perClientReceiveTotal = (MultipleThreadTest.this.messageCount * MultipleThreadTest.this.threadCount);
int serverReceiveTotal = perClientReceiveTotal * MultipleThreadTest.this.clientCount;
int perClientReceiveTotal = (this.messageCount * this.threadCount);
int serverReceiveTotal = perClientReceiveTotal * this.clientCount;
AtomicInteger sent = new AtomicInteger(0);
AtomicInteger totalClientReceived = new AtomicInteger(0);
@ -57,6 +62,14 @@ class MultipleThreadTest extends BaseTest {
@Test
public
void multipleThreads() throws SecurityException, IOException {
// our clients should receive messageCount * threadCount * clientCount TOTAL messages
final int totalClientReceivedCountExpected = this.clientCount * this.messageCount * this.threadCount;
final int totalServerReceivedCountExpected = this.clientCount * this.messageCount;
System.err.println("CLIENT RECEIVES: " + totalClientReceivedCountExpected);
System.err.println("SERVER RECEIVES: " + totalServerReceivedCountExpected);
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
configuration.host = host;
@ -66,6 +79,7 @@ class MultipleThreadTest extends BaseTest {
final Server server = new Server(configuration);
server.disableRemoteKeyValidation();
addEndPoint(server);
server.bind(false);
@ -94,8 +108,10 @@ class MultipleThreadTest extends BaseTest {
//System.err.println(dataClass.data);
MultipleThreadTest.this.sentStringsToClientDebug.put(incrementAndGet, dataClass);
connection.send()
.TCP(dataClass);
.TCP(dataClass)
.flush();
}
}
}.start();
}
@ -107,20 +123,31 @@ class MultipleThreadTest extends BaseTest {
public
void received(Connection connection, DataClass object) {
int incrementAndGet = MultipleThreadTest.this.receivedServer.getAndIncrement();
//System.err.println("server #" + incrementAndGet);
if (incrementAndGet == serverReceiveTotal) {
System.err.println("Server DONE " + incrementAndGet);
stopEndPoints();
if (incrementAndGet % MultipleThreadTest.this.messageCount == 0) {
System.err.println("Server receive DONE for client " + incrementAndGet);
stillRunning = false;
synchronized (MultipleThreadTest.this.lock) {
MultipleThreadTest.this.lock.notifyAll();
}
}
if (incrementAndGet == totalServerReceivedCountExpected) {
System.err.println("Server DONE: " + incrementAndGet);
finalStillRunning = false;
synchronized (MultipleThreadTest.this.finalRunLock) {
MultipleThreadTest.this.finalRunLock.notifyAll();
}
}
}
});
// ----
finalStillRunning = true;
for (int i = 1; i <= this.clientCount; i++) {
final int index = i;
@ -154,27 +181,36 @@ class MultipleThreadTest extends BaseTest {
}
}
});
stillRunning = true;
client.connect(5000);
while (stillRunning) {
synchronized (this.lock) {
try {
this.lock.wait(5 * 1000); // 5 secs
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
while (finalStillRunning) {
synchronized (this.finalRunLock) {
try {
this.finalRunLock.wait(5 * 1000); // 5 secs
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// CLIENT will wait until it's done connecting, but SERVER is async.
// the ONLY way to safely work in the server is with LISTENERS. Everything else can FAIL, because of it's async nature.
// our clients should receive messageCount * threadCount * clientCount TOTAL messages
int totalClientReceivedCountExpected = this.threadCount * this.clientCount * this.messageCount;
int totalServerReceivedCountExpected = this.clientCount * this.messageCount;
System.err.println("CLIENT RECEIVES: " + totalClientReceivedCountExpected);
System.err.println("SERVER RECEIVES: " + totalServerReceivedCountExpected);
synchronized (this.lock) {
try {
this.lock.wait(5 * 1000); // 5 secs
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!this.sentStringsToClientDebug.isEmpty()) {
System.err.println("MISSED DATA: " + this.sentStringsToClientDebug.size());
for (Map.Entry<Integer, DataClass> i : this.sentStringsToClientDebug.entrySet()) {
@ -184,7 +220,9 @@ class MultipleThreadTest extends BaseTest {
stopEndPoints();
assertEquals(totalClientReceivedCountExpected, totalClientReceived.get());
assertEquals(totalServerReceivedCountExpected, this.receivedServer.get() - 1); // offset by 1 since we start at 1.
// offset by 1 since we start at 1
assertEquals(totalServerReceivedCountExpected, receivedServer.get()-1);
}

View File

@ -33,7 +33,7 @@ import dorkbox.util.exceptions.SecurityException;
public
class ReconnectTest extends BaseTest {
AtomicInteger receivedCount;
private final AtomicInteger receivedCount = new AtomicInteger(0);
@Test
public
@ -55,7 +55,7 @@ class ReconnectTest extends BaseTest {
private
void socketReuse(final boolean useTCP, final boolean useUDP) throws SecurityException, IOException {
this.receivedCount = new AtomicInteger(0);
receivedCount.set(0);
Configuration configuration = new Configuration();
configuration.host = host;
@ -92,6 +92,10 @@ class ReconnectTest extends BaseTest {
void received(Connection connection, String object) {
int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet();
System.out.println("----- <S " + connection + "> " + incrementAndGet + " : " + object);
synchronized (receivedCount) {
receivedCount.notifyAll();
}
}
});
@ -120,32 +124,45 @@ class ReconnectTest extends BaseTest {
void received(Connection connection, String object) {
int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet();
System.out.println("----- <C " + connection + "> " + incrementAndGet + " : " + object);
synchronized (receivedCount) {
receivedCount.notifyAll();
}
}
});
server.bind(false);
int count = 10;
int count = 100;
int initialCount = 2;
if (useTCP && useUDP) {
initialCount += 2;
}
for (int i = 1; i < count + 1; i++) {
System.out.println(".....");
client.connect(5000);
int waitingRetryCount = 10;
int waitingRetryCount = 20;
int target = i * initialCount;
while (this.receivedCount.get() != target) {
if (waitingRetryCount-- < 0) {
throw new IOException("Invalid target count...");
}
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
synchronized (receivedCount) {
while (this.receivedCount.get() != target) {
if (waitingRetryCount-- < 0) {
System.out.println("Aborting...");
stopEndPoints();
assertEquals(target, this.receivedCount.get());
}
try {
receivedCount.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
client.closeConnections();
client.close();
System.out.println(".....");
}
assertEquals(count * initialCount, this.receivedCount.get());
@ -157,7 +174,7 @@ class ReconnectTest extends BaseTest {
@Test
public
void localReuse() throws SecurityException, IOException {
this.receivedCount = new AtomicInteger(0);
receivedCount.set(0);
Server server = new Server();
addEndPoint(server);
@ -167,7 +184,7 @@ class ReconnectTest extends BaseTest {
public
void connected(Connection connection) {
connection.send()
.TCP("-- LOCAL from server");
.self("-- LOCAL from server");
}
});
server.listeners()
@ -190,7 +207,7 @@ class ReconnectTest extends BaseTest {
public
void connected(Connection connection) {
connection.send()
.TCP("-- LOCAL from client");
.self("-- LOCAL from client");
}
});
@ -209,16 +226,16 @@ class ReconnectTest extends BaseTest {
for (int i = 1; i < count + 1; i++) {
client.connect(5000);
int target = i;
int target = i * 2;
while (this.receivedCount.get() != target) {
System.out.println("----- Waiting...");
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
} catch (InterruptedException ignored) {
}
}
client.closeConnections();
client.close();
}
assertEquals(count * 2, this.receivedCount.get());

View File

@ -34,7 +34,9 @@
*/
package dorkbox.network.rmi;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
@ -166,12 +168,14 @@ class RmiGlobalTest extends BaseTest {
// Test sending a reference to a remote object (the receiving end should receive the IMPL object, not the proxy object)
System.out.println("Sending proxied object to remote...");
MessageWithTestCow m = new MessageWithTestCow(test);
m.number = 678;
m.text = "sometext";
connection.send()
.TCP(m);
.TCP(m)
.flush();
}

View File

@ -30,6 +30,7 @@ import dorkbox.network.Server;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Listener;
import dorkbox.network.connection.bridge.ConnectionBridge;
import dorkbox.network.serialization.Serialization;
import dorkbox.util.exceptions.SecurityException;
@ -39,7 +40,7 @@ class RmiSendObjectOverrideMethodTest extends BaseTest {
@Test
public
void rmiNetwork() throws SecurityException, IOException {
void rmiTcp() throws SecurityException, IOException {
rmi(new Config() {
@Override
public
@ -50,6 +51,19 @@ class RmiSendObjectOverrideMethodTest extends BaseTest {
});
}
@Test
public
void rmiUdp() throws SecurityException, IOException {
rmi(new Config() {
@Override
public
void apply(final Configuration configuration) {
configuration.udpPort = udpPort;
configuration.host = host;
}
});
}
@Test
public
void rmiLocal() throws SecurityException, IOException {
@ -90,6 +104,8 @@ class RmiSendObjectOverrideMethodTest extends BaseTest {
Configuration configuration = new Configuration();
config.apply(configuration);
final boolean isUDP = configuration.udpPort > 0;
configuration.serialization = Serialization.DEFAULT();
configuration.serialization.registerRmiImplementation(TestObject.class, TestObjectImpl.class);
configuration.serialization.registerRmiImplementation(OtherObject.class, OtherObjectImpl.class);
@ -164,8 +180,15 @@ class RmiSendObjectOverrideMethodTest extends BaseTest {
// When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because
// that is where that object acutally exists.
// we have to manually flush, since we are in a separate thread that does not auto-flush.
connection.send()
.TCP(otherObject);
ConnectionBridge send = connection.send();
if (isUDP) {
send.UDP(otherObject)
.flush();
} else {
send.TCP(otherObject)
.flush();
}
}
}.start();
}

View File

@ -163,7 +163,8 @@ class RmiSendObjectTest extends BaseTest {
// When a remote proxy object is sent, the other side receives its actual remote object.
// we have to manually flush, since we are in a separate thread that does not auto-flush.
connection.send()
.TCP(otherObject);
.TCP(otherObject)
.flush();
}
}.start();
}

View File

@ -34,7 +34,9 @@
*/
package dorkbox.network.rmi;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
@ -160,7 +162,8 @@ class RmiTest extends BaseTest {
m.number = 678;
m.text = "sometext";
connection.send()
.TCP(m);
.TCP(m)
.flush();
System.out.println("Finished tests");
}