WIP - finished global/connection specific RMI, IPC aeron connectivity

old_release
nathan 2020-07-06 16:36:56 +02:00
parent fc7baa6c8d
commit 1608c0d6a2
80 changed files with 2500 additions and 4508 deletions

View File

@ -283,6 +283,9 @@ tasks.withType<KotlinCompile> {
jvmTarget = Extras.JAVA_VERSION
apiVersion = Extras.KOTLIN_API_VERSION
languageVersion = Extras.KOTLIN_LANG_VERSION
// enable the use of inline classes. see https://kotlinlang.org/docs/reference/inline-classes.html
freeCompilerArgs += "-Xinline-classes"
}
}
@ -345,11 +348,11 @@ dependencies {
implementation("com.dorkbox:ObjectPool:2.12")
implementation("com.dorkbox:Utilities:1.5.3")
// https://github.com/MicroUtils/kotlin-logging
implementation("io.github.microutils:kotlin-logging:1.7.9") // slick kotlin wrapper for slf4j
implementation("org.slf4j:slf4j-api:1.7.30")
testImplementation("junit:junit:4.13")
testImplementation("ch.qos.logback:logback-classic:1.2.3")
}

View File

@ -18,6 +18,8 @@ package dorkbox.network
import dorkbox.network.aeron.client.ClientException
import dorkbox.network.aeron.client.ClientTimedOutException
import dorkbox.network.connection.*
import dorkbox.network.other.NetUtil
import dorkbox.network.other.NetworkUtil
import dorkbox.util.exceptions.SecurityException
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.launch
@ -138,7 +140,7 @@ open class Client<C : Connection>(config: Configuration = Configuration()) : End
*/
@JvmOverloads
@Throws(IOException::class, ClientTimedOutException::class)
suspend fun connect(remoteAddress: String = "localhost", connectionTimeoutMS: Long = 30_000L, reliable: Boolean = true) {
suspend fun connect(remoteAddress: String = "", connectionTimeoutMS: Long = 30_000L, reliable: Boolean = true) {
if (isConnected.value) {
throw IOException("Unable to connect when already connected!");
}
@ -166,26 +168,52 @@ open class Client<C : Connection>(config: Configuration = Configuration()) : End
throw IllegalArgumentException("0.0.0.0 is an invalid address to connect to!")
}
connectionManager.init(this)
// this is an IPC address
if (this.remoteAddress.startsWith("0x")) {
val ipcAddress: Long
try {
ipcAddress = remoteAddress.toLong(radix = 16)
} catch (e: Exception) {
throw IOException(e)
}
if (this.remoteAddress.isEmpty()) {
// this is an IPC address
// val connectionType3 = ConnectionType(config.remoteAddress, config.controlPort, config.port, false, MediaDriverType.IPC, IPC_STREAM_ID)
// When conducting IPC transfers, we MUST use the same aeron configuration as the server!
// config.aeronLogDirectory
// stream IDs are flipped for a client because we operate from the perspective of the server
val handshakeConnection = IpcMediaDriverConnection(
streamId = IPC_HANDSHAKE_STREAM_ID_SUB,
streamIdSubscription = IPC_HANDSHAKE_STREAM_ID_PUB,
sessionId = RESERVED_SESSION_ID_INVALID
)
closables.add(handshakeConnection)
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports
handshakeConnection.buildClient(aeron)
// logger.debug(handshakeConnection.clientInfo())
println("CONASD")
// this will block until the connection timeout, and throw an exception if we were unable to connect with the server
// @Throws(ConnectTimedOutException::class, ClientRejectedException::class)
val connectionInfo = connectionManager.initHandshake(handshakeConnection, connectionTimeoutMS)
println("CO23232232323NASD")
}
else {
// this is a network address
// THIS IS A NETWORK ADDRESS
// initially we only connect to the client connect ports. Ports are flipped because they are in the perspective of the SERVER
// initially we only connect to the handshake connect ports. Ports are flipped because they are in the perspective of the SERVER
val handshakeConnection = UdpMediaDriverConnection(
this.remoteAddress, config.publicationPort, config.subscriptionPort,
UDP_HANDSHAKE_STREAM_ID, RESERVED_SESSION_ID_INVALID,
connectionTimeoutMS, reliable)
address = this.remoteAddress,
subscriptionPort = config.publicationPort,
publicationPort = config.subscriptionPort,
streamId = UDP_HANDSHAKE_STREAM_ID,
sessionId = RESERVED_SESSION_ID_INVALID,
connectionTimeoutMS = connectionTimeoutMS,
isReliable = reliable)
closables.add(handshakeConnection)
@ -197,12 +225,18 @@ open class Client<C : Connection>(config: Configuration = Configuration()) : End
// this will block until the connection timeout, and throw an exception if we were unable to connect with the server
// @Throws(ConnectTimedOutException::class, ClientRejectedException::class)
val connectionInfo = connectionManager.initHandshake(handshakeConnection, connectionTimeoutMS, this@Client)
val connectionInfo = connectionManager.initHandshake(handshakeConnection, connectionTimeoutMS)
// we are now connected, so we can connect to the NEW client-specific ports
val reliableClientConnection = UdpMediaDriverConnection(handshakeConnection.address, connectionInfo.subscriptionPort, connectionInfo.publicationPort,
connectionInfo.streamId, connectionInfo.sessionId, connectionTimeoutMS, handshakeConnection.isReliable)
val reliableClientConnection = UdpMediaDriverConnection(
address = handshakeConnection.address,
subscriptionPort = connectionInfo.subscriptionPort,
publicationPort = connectionInfo.publicationPort,
streamId = connectionInfo.streamId,
sessionId = connectionInfo.sessionId,
connectionTimeoutMS = connectionTimeoutMS,
isReliable = handshakeConnection.isReliable)
// VALIDATE:: check to see if the remote connection's public key has changed!
if (!validateRemoteAddress(NetworkUtil.IP.toInt(this.remoteAddress), connectionInfo.publicKey)) {
@ -308,77 +342,48 @@ open class Client<C : Connection>(config: Configuration = Configuration()) : End
// }
/**
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
* to an object that is created remotely.
*
* Tells the remote connection to create a new global object that implements the specified interface.
*
* The methods on the returned object will remotely execute on the (remotely) created object
* The callback will be notified when the remote object has been created.
*
*
*
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* [response timeout][RemoteObject.setResponseTimeout].
*
*
* If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must
* not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a
* void return value can be called on the update thread.
*
* If you want to create a connection specific remote object, call [Connection.create(Int, RemoteObjectCallback<Iface>)] on a connection
* The callback will be notified when the remote object has been created.
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `RemoteObject remoteObject = (RemoteObject) test;`
* If one wishes to change the remote object behavior, cast the object [RemoteObject] to access the different methods, for example:
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
// override fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>) {
// try {
// connection!!.createRemoteObject(interfaceClass, callback)
// } catch (e: NullPointerException) {
// logger.error("Error creating remote object!", e)
// }
// }
suspend inline fun <reified Iface> createObject(noinline callback: suspend (Iface) -> Unit) {
val classId = serialization.getClassId(Iface::class.java)
rmiSupport.createGlobalRemoteObject(getConnection(), classId, callback)
}
/**
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
* to an object that is created remotely.
* Gets a global remote object via the ID.
*
* Global remote objects are accessible to ALL connections, where as a connection specific remote object is only accessible/visible
* to the connection.
*
* If you want to access a connection specific remote object, call [Connection.get(Int, RemoteObjectCallback<Iface>)] on a connection
* The callback will be notified when the remote object has been created.
*
*
*
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* [response timeout][RemoteObject.setResponseTimeout].
*
*
* If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must
* not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a
* void return value can be called on the update thread.
*
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `RemoteObject remoteObject = (RemoteObject) test;`
* If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example:
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
// override fun <Iface> getRemoteObject(objectId: Int, callback: RemoteObjectCallback<Iface>) {
// try {
// connection!!.getRemoteObject(objectId, callback)
// } catch (e: NullPointerException) {
// logger.error("Error getting remote object!", e)
// }
// }
fun <Iface> getObject(objectId: Int, interfaceClass: Class<Iface>): Iface {
return rmiSupport.getGlobalRemoteObject(getConnection(), this, objectId, interfaceClass)
}
/**
* Fetches the connection used by the client.
@ -389,14 +394,9 @@ open class Client<C : Connection>(config: Configuration = Configuration()) : End
*
* This is preferred to [EndPoint.getConnections], as it properly does some error checking
*/
// can =just use super.get connection?
// override var connection: C = TODO()
// get() = field
// set(connection) {
// super.connection = connection
// }
fun getConnection(): C {
return connection as C
}
@Throws(ClientException::class)
suspend fun send(message: Any) {

View File

@ -16,14 +16,13 @@
package dorkbox.network
import dorkbox.network.aeron.server.ServerException
import dorkbox.network.connection.Connection
import dorkbox.network.connection.ConnectionManagerServer
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.UdpMediaDriverConnection
import dorkbox.network.connection.*
import dorkbox.network.connection.connectionType.ConnectionProperties
import dorkbox.network.connection.connectionType.ConnectionRule
import dorkbox.network.ipFilter.IpFilterRule
import dorkbox.network.ipFilter.IpFilterRuleType
import dorkbox.network.other.NetUtil
import dorkbox.network.other.NetworkUtil
import io.aeron.FragmentAssembler
import io.aeron.logbuffer.FragmentHandler
import io.aeron.logbuffer.Header
@ -146,8 +145,11 @@ open class Server<C : Connection>(config: ServerConfiguration = ServerConfigurat
// The is how clients then get the new ports to connect to + other configuration options
val handshakeDriver = UdpMediaDriverConnection(
config.listenIpAddress, config.subscriptionPort, config.publicationPort,
UDP_HANDSHAKE_STREAM_ID, RESERVED_SESSION_ID_INVALID)
address = config.listenIpAddress,
subscriptionPort = config.subscriptionPort,
publicationPort = config.publicationPort,
streamId = UDP_HANDSHAKE_STREAM_ID,
sessionId = RESERVED_SESSION_ID_INVALID)
handshakeDriver.buildServer(aeron)
@ -155,7 +157,19 @@ open class Server<C : Connection>(config: ServerConfiguration = ServerConfigurat
val handshakeSubscription = handshakeDriver.subscription
logger.debug(handshakeDriver.serverInfo())
logger.debug("Server listening for incomming clients on ${handshakePublication.localSocketAddresses()}")
logger.debug("Server listening for incoming clients on ${handshakePublication.localSocketAddresses()}")
val ipcHandshakeDriver = IpcMediaDriverConnection(
streamId = IPC_HANDSHAKE_STREAM_ID_PUB,
streamIdSubscription = IPC_HANDSHAKE_STREAM_ID_SUB,
sessionId = RESERVED_SESSION_ID_INVALID
)
ipcHandshakeDriver.buildServer(aeron)
val ipcHandshakePublication = ipcHandshakeDriver.publication
val ipcHandshakeSubscription = ipcHandshakeDriver.subscription
/**
* Note:
@ -171,17 +185,29 @@ open class Server<C : Connection>(config: ServerConfiguration = ServerConfigurat
connectionManager.receiveHandshakeMessageServer(handshakePublication, buffer, offset, length, header, this@Server)
}
})
val ipcInitialConnectionHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
actionDispatch.launch {
println("GOT MESSAGE!")
}
})
actionDispatch.launch {
val pollIdleStrategy = config.pollIdleStrategy
try {
var pollCount: Int
while (!isShutdown()) {
pollCount = 0
// this checks to see if there are NEW clients
var pollCount = handshakeSubscription.poll(initialConnectionHandler, 100)
// pollCount += handshakeSubscription.poll(initialConnectionHandler, 100)
// this checks to see if there are NEW clients via IPC
pollCount += ipcHandshakeSubscription.poll(ipcInitialConnectionHandler, 100)
// this manages existing clients (for cleanup + connection polling)
pollCount += connectionManager.poll()
// pollCount += connectionManager.poll()
// 0 means we idle. >0 means reset and don't idle (because there are likely more poll events)
pollIdleStrategy.idle(pollCount)
@ -189,6 +215,9 @@ open class Server<C : Connection>(config: ServerConfiguration = ServerConfigurat
} finally {
handshakePublication.close()
handshakeSubscription.close()
ipcHandshakePublication.close()
ipcHandshakeSubscription.close()
}
}
@ -210,7 +239,7 @@ open class Server<C : Connection>(config: ServerConfiguration = ServerConfigurat
* If there is nothing added to this list - then ALL are permitted
*/
fun addIpFilterRule(vararg rules: IpFilterRule?) {
ipFilterRules.addAll(Arrays.asList(*rules))
ipFilterRules.addAll(listOf(*rules))
}
/**
@ -299,6 +328,35 @@ open class Server<C : Connection>(config: ServerConfiguration = ServerConfigurat
// connectionManager.removeListenerManager(connection)
// }
//
//
// /**
// * Creates a "global" remote object for use by multiple connections.
// *
// * @return the ID assigned to this RMI object
// */
// fun <T> create(objectId: Short, globalObject: T) {
// return rmiGlobalObjects.register(globalObject)
// }
//
// /**
// * Creates a "global" remote object for use by multiple connections.
// *
// * @return the ID assigned to this RMI object
// */
// fun <T> create(`object`: T): Short {
// return rmiGlobalObjects.register(`object`) ?: 0
// }
//
//
//
/**
* Adds a custom connection to the server.
*

View File

@ -326,6 +326,13 @@ class CoroutineBackoffIdleStrategy : BackoffIdleStrategyData, CoroutineIdleStrat
return ALIAS
}
/**
* Creates a clone of this IdleStrategy
*/
override fun clone(): CoroutineBackoffIdleStrategy {
return CoroutineBackoffIdleStrategy(maxSpins = maxSpins, maxYields = maxYields, minParkPeriodMs = minParkPeriodMs, maxParkPeriodMs = maxParkPeriodMs)
}
override fun toString(): String {
return "BackoffIdleStrategy{" +
"alias=" + ALIAS +

View File

@ -102,4 +102,9 @@ interface CoroutineIdleStrategy {
fun alias(): String {
return ""
}
/**
* Creates a clone of this IdleStrategy
*/
fun clone(): CoroutineIdleStrategy
}

View File

@ -91,6 +91,14 @@ class CoroutineSleepingMillisIdleStrategy : CoroutineIdleStrategy {
return ALIAS
}
/**
* Creates a clone of this IdleStrategy
*/
override fun clone(): CoroutineSleepingMillisIdleStrategy {
return CoroutineSleepingMillisIdleStrategy(sleepPeriodMs = sleepPeriodMs)
}
override fun toString(): String {
return "SleepingMillisIdleStrategy{" +
"alias=" + ALIAS +

View File

@ -2,11 +2,11 @@ package dorkbox.network.connection
import org.slf4j.Logger
data class ClientConnectionInfo(val subscriptionPort: Int,
val publicationPort: Int,
val sessionId: Int,
val streamId: Int,
val publicKey: ByteArray) {
class ClientConnectionInfo(val subscriptionPort: Int,
val publicationPort: Int,
val sessionId: Int,
val streamId: Int,
val publicKey: ByteArray) {
fun log(handshakeSessionId: Int, logger: Logger) {
logger.debug("[{}] connect {} {} (encrypted {})", handshakeSessionId, subscriptionPort, publicationPort, sessionId)

View File

@ -15,8 +15,6 @@
*/
package dorkbox.network.connection
import dorkbox.network.rmi.RemoteObjectCallback
interface Connection : AutoCloseable {
/**
* Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed.
@ -121,16 +119,6 @@ interface Connection : AutoCloseable {
override fun close()
// TODO: below should just be "new()" to create a new object, to mirror "new Object()"
// // RMI
// // client.get(5) -> gets from the server connection, if exists, then global.
// // on server, a connection local RMI object "uses" an id for global, so there will never be a conflict
// // using some tricks, we can make it so that it DOESN'T matter the order in which objects are created,
// // and can specify, if we want, the object created.
// // Once created though, as NEW ONE with the same ID cannot be created until the old one is removed!
/**
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
* to an object that is created remotely.
@ -158,7 +146,7 @@ interface Connection : AutoCloseable {
*
* @see RemoteObject
*/
suspend fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>)
suspend fun <Iface> createObject(callback: suspend (Iface) -> Unit)
/**
* Tells the remote connection to access an already created proxy object that implements the specified interface. The methods on this object "map"
@ -188,5 +176,5 @@ interface Connection : AutoCloseable {
*
* @see RemoteObject
*/
suspend fun <Iface> getRemoteObject(objectId: Int, callback: RemoteObjectCallback<Iface>)
fun <Iface> getObject(objectId: Int, interfaceClass: Class<Iface>): Iface
}

View File

@ -15,12 +15,12 @@
*/
package dorkbox.network.connection
import dorkbox.network.NetworkUtil
import dorkbox.network.Server
import dorkbox.network.connection.ping.PingFuture
import dorkbox.network.connection.ping.PingMessage
import dorkbox.network.rmi.ConnectionRmiSupport
import dorkbox.network.rmi.RemoteObjectCallback
import dorkbox.network.other.NetworkUtil
import dorkbox.network.rmi.RmiSupportConnection
import dorkbox.util.classes.ClassHelper
import io.aeron.FragmentAssembler
import io.aeron.Publication
import io.aeron.Subscription
@ -33,7 +33,6 @@ import org.agrona.BitUtil
import org.agrona.BufferUtil
import org.agrona.DirectBuffer
import org.agrona.concurrent.UnsafeBuffer
import org.slf4j.Logger
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@ -57,10 +56,13 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
final override val sessionId: Int
private val serialization = endPoint.config.serialization
private val sendIdleStrategy = endPoint.config.sendIdleStrategy.clone()
val expirationTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(endPoint.config.connectionCleanupTimeoutInSeconds.toLong())
private val expirationTime = System.currentTimeMillis() +
TimeUnit.SECONDS.toMillis(endPoint.config.connectionCleanupTimeoutInSeconds.toLong())
private val logger: Logger = endPoint.logger
private val logger = endPoint.logger
// private val needsLock = AtomicBoolean(false)
// private val writeSignalNeeded = AtomicBoolean(false)
@ -76,8 +78,8 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
private var pingFuture: PingFuture? = null
// used to store connection local listeners (instead of global listeners). Only possible on the server.
@Volatile
private var localListenerManager: ConnectionManager<*>? = null
// @Volatile
// private var localListenerManager: ConnectionManager<*>? = null
// while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error.
private var remoteKeyChanged = false
@ -91,7 +93,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
private var closeLatch: CountDownLatch? = null
// RMI support for this connection
var rmiSupport: ConnectionRmiSupport
private val rmiSupportConnection: RmiSupportConnection<Connection_>
var messageHandler: FragmentAssembler
@ -119,7 +121,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
publicationPort = mediaDriverConnection.publicationPort
remoteAddress = mediaDriverConnection.address
remoteAddressInt = NetworkUtil.IP.toInt(remoteAddress)
streamId = mediaDriverConnection.streamId
streamId = mediaDriverConnection.streamId // NOTE: this is UNIQUE per server!
sessionId = mediaDriverConnection.sessionId
messageHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
@ -130,7 +132,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
}
})
rmiSupport = ConnectionRmiSupport(endPoint.rmiGlobalBridge)
rmiSupportConnection = RmiSupportConnection(logger, endPoint.rmiSupport, endPoint.serialization, endPoint.actionDispatch)
// when closing this connection, HOW MANY endpoints need to be closed?
closeLatch = CountDownLatch(1)
@ -139,8 +141,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
/**
* @param now The current time
*
* @return `true` if this duologue has no subscribers and the current
* time `now` is after the intended expiry date of the duologue
* @return `true` if this connection has no subscribers and the current time `now` is after the expriation date
*/
override fun isExpired(now: Long): Boolean {
return subscription.imageCount() == 0 && now > expirationTime
@ -290,7 +291,42 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
* Safely sends objects to a destination (such as a custom object or a standard ping).
*/
override suspend fun send(message: Any) {
endPoint.writeMessage(publication, this, message)
// The sessionId is globally unique, and is assigned by the server.
logger.debug("[{}] send: {}", publication.sessionId(), message)
val kryo: KryoExtra = serialization.takeKryo()
try {
kryo.write(this, message)
val buffer = kryo.writerBuffer
val objectSize = buffer.position()
val internalBuffer = buffer.internalBuffer
var result: Long
while (true) {
result = publication.offer(internalBuffer, 0, objectSize)
// success!
if (result > 0) {
return
}
if (result == Publication.BACK_PRESSURED || result == Publication.ADMIN_ACTION) {
// we should retry.
sendIdleStrategy.idle()
continue
}
// more critical error sending the message. we shouldn't retry or anything.
logger.error("Error sending message. ${EndPoint.errorCodeName(result)}")
return
}
} catch (e: Exception) {
logger.error("Error serializing message $message", e)
} finally {
sendIdleStrategy.reset()
serialization.returnKryo(kryo)
}
}
@ -359,9 +395,10 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
// remove all RMI listeners
rmiSupport.close()
// rmiSupport.close() // TODO
}
// @Throws(Exception::class)
// override fun exceptionCaught(context: ChannelHandlerContext, cause: Throwable) {
// val channel = context.channel()
@ -500,7 +537,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
* This includes all proxy listeners
*/
override fun removeAll(): Listeners<Connection> {
rmiSupport.removeAllListeners()
// rmiSupport.removeAllListeners() // TODO
if (endPoint is Server) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level
@ -600,15 +637,18 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
// RMI methods
//
//
override fun rmiSupport(): ConnectionRmiSupport {
return rmiSupport
override fun rmiSupport(): RmiSupportConnection<Connection_> {
return rmiSupportConnection
}
override suspend fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>) {
rmiSupport.createRemoteObject(this, interfaceClass, callback)
override suspend fun <Iface> createObject(callback: suspend (Iface) -> Unit) {
val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function2::class.java, callback.javaClass, 0)
val interfaceClassId = endPoint.serialization.getClassId(iFaceClass)
rmiSupportConnection.createRemoteObject(this, interfaceClassId, callback)
}
override suspend fun <Iface> getRemoteObject(objectId: Int, callback: RemoteObjectCallback<Iface>) {
rmiSupport.getRemoteObject(this, objectId, callback)
override fun <Iface> getObject(objectId: Int, interfaceClass: Class<Iface>): Iface {
@Suppress("UNCHECKED_CAST")
return rmiSupportConnection.getRemoteObject(this, endPoint as EndPoint<Connection_>, objectId, interfaceClass)
}
}

View File

@ -22,12 +22,15 @@ class ConnectionManagerClient<C : Connection>(logger: Logger, config: Configurat
private var failed = false
lateinit var handler: FragmentHandler
lateinit var endPoint: EndPoint<C>
var sessionId: Int = 0
@Throws(ClientTimedOutException::class, ClientRejectedException::class)
suspend fun initHandshake(mediaConnection: MediaDriverConnection, connectionTimeoutMS: Long, endPoint: EndPoint<C>) : ClientConnectionInfo {
// now we have a bi-directional connection with the server on the handshake socket.
val handler: FragmentHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
fun init(endPoint: EndPoint<C>) {
this.endPoint = endPoint
// now we have a bi-directional connection with the server on the handshake "socket".
handler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
endPoint.actionDispatch.launch {
val message = endPoint.readHandshakeMessage(buffer, offset, length, header)
logger.debug("[{}] response: {}", sessionId, message)
@ -54,19 +57,25 @@ class ConnectionManagerClient<C : Connection>(logger: Logger, config: Configurat
subscriptionPort = message.publicationPort,
publicationPort = message.subscriptionPort,
sessionId = oneTimePad xor message.oneTimePad,
streamId = oneTimePad xor message.streamId,
streamId = oneTimePad xor message.streamId,
publicKey = message.publicKey!!)
connectionInfo!!.log(sessionId, logger)
}
})
}
val registrationMessage = Registration.hello(oneTimePad, config.settingsStore.getPublicKey()!!)
@Throws(ClientTimedOutException::class, ClientRejectedException::class)
suspend fun initHandshake(mediaConnection: MediaDriverConnection, connectionTimeoutMS: Long) : ClientConnectionInfo {
val registrationMessage = Registration.hello(
oneTimePad = oneTimePad,
publicKey = config.settingsStore.getPublicKey()!!,
registrationData = config.serialization.getKryoRegistrationDetails()
)
// Send the one-time pad to the server.
endPoint.writeMessage(mediaConnection.publication, registrationMessage)
endPoint.writeHandshakeMessage(mediaConnection.publication, registrationMessage)
sessionId = mediaConnection.publication.sessionId()

View File

@ -1,6 +1,5 @@
package dorkbox.network.connection
import dorkbox.network.NetworkUtil
import dorkbox.network.ServerConfiguration
import dorkbox.network.aeron.client.ClientRejectedException
import dorkbox.network.aeron.server.AllocationException
@ -8,6 +7,7 @@ import dorkbox.network.aeron.server.PortAllocator
import dorkbox.network.aeron.server.RandomIdAllocator
import dorkbox.network.aeron.server.ServerException
import dorkbox.network.connection.registration.Registration
import dorkbox.network.other.NetworkUtil
import io.aeron.Image
import io.aeron.Publication
import io.aeron.logbuffer.Header
@ -46,37 +46,36 @@ class ConnectionManagerServer<C : Connection>(logger: Logger,
suspend fun receiveHandshakeMessageServer(handshakePublication: Publication,
buffer: DirectBuffer, offset: Int, length: Int, header: Header,
endPoint: EndPoint<C>) {
// TODO: notify error callbacks if there is an exception!
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
// For the handshake, the sessionId IS NOT GLOBALLY UNIQUE
val sessionId = header.sessionId()
// note: this address will ALWAYS be an IP:PORT combo
val remoteIpAndPort = (header.context() as Image).sourceIdentity()
// split
val splitPoint = remoteIpAndPort.lastIndexOf(':')
val clientAddressString = remoteIpAndPort.substring(0, splitPoint)
// val port = remoteIpAndPort.substring(splitPoint+1)
val clientAddress = NetworkUtil.IP.toInt(clientAddressString)
config as ServerConfiguration
val message = endPoint.readHandshakeMessage(buffer, offset, length, header)
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (message !is Registration) {
endPoint.writeHandshakeMessage(handshakePublication, Registration.error("Invalid connection request"))
return
}
try {
// split
val splitPoint = remoteIpAndPort.lastIndexOf(':')
val clientAddressString = remoteIpAndPort.substring(0, splitPoint)
// val port = remoteIpAndPort.substring(splitPoint+1)
val clientAddress = NetworkUtil.IP.toInt(clientAddressString)
// TODO: notify error if there is an exceptoin!
val message = endPoint.readHandshakeMessage(buffer, offset, length, header)
logger.debug("[{}] received: {}", sessionId, message)
config as ServerConfiguration
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (message !is Registration) {
endPoint.writeMessage(handshakePublication, Registration.error("Invalid connection request"))
return
}
// VALIDATE:: Check to see if there are already too many clients connected.
if (connectionCount() >= config.maxClientCount) {
logger.debug("server is full")
endPoint.writeMessage(handshakePublication, Registration.error("server full. Max allowed is ${config.maxClientCount}"))
endPoint.writeHandshakeMessage(handshakePublication, Registration.error("server full. Max allowed is ${config.maxClientCount}"))
return
}
@ -87,8 +86,12 @@ class ConnectionManagerServer<C : Connection>(logger: Logger,
return
}
// VALIDATE TODO: make sure the serialization matches between the client/server!
// VALIDATE:: make sure the serialization matches between the client/server!
if (!config.serialization.verifyKryoRegistration(message.registrationData!!)) {
// TODO: this should provide info to a callback
println("connection not allowed! registration data mismatch")
return
}
// VALIDATE:: we are now connected to the client and are going to create a new connection.
val currentCountForIp = connectionsPerIpCounts.getAndIncrement(clientAddress)
@ -97,109 +100,82 @@ class ConnectionManagerServer<C : Connection>(logger: Logger,
connectionsPerIpCounts.getAndDecrement(clientAddress)
logger.debug("too many connections for IP address")
endPoint.writeMessage(handshakePublication, Registration.error("too many connections for IP address. Max allowed is ${config.maxConnectionsPerIpAddress}"))
endPoint.writeHandshakeMessage(handshakePublication, Registration.error("too many connections for IP address. Max allowed is ${config.maxConnectionsPerIpAddress}"))
return
}
} catch (e: Exception) {
logger.error("could not validate client message: ", e)
}
// VALIDATE:: TODO: ?? check to see if this session is ALREADY connected??. It should not be!
// VALIDATE:: TODO: ?? check to see if this session is ALREADY connected??. It should not be!
/////
/////
///// DONE WITH VALIDATION
/////
/////
/////
/////
///// DONE WITH VALIDATION
/////
/////
// allocate ports for the client
val connectionPorts: IntArray
try {
// throws exception if this is not possible
connectionPorts = portAllocator.allocate(portsPerClient)
} catch (e: IllegalArgumentException) {
// have to unwind actions!
connectionsPerIpCounts.getAndDecrement(clientAddress)
logger.error("Unable to allocate $portsPerClient ports for client connection!")
return
}
// allocate session/stream id's
val connectionSessionId: Int
try {
connectionSessionId = sessionIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
connectionsPerIpCounts.getAndDecrement(clientAddress)
portAllocator.free(connectionPorts)
logger.error("Unable to allocate a session ID for the client connection!")
return
}
// allocate ports for the client
val connectionPorts: IntArray
val connectionStreamId: Int
try {
connectionStreamId = streamIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
connectionsPerIpCounts.getAndDecrement(clientAddress)
portAllocator.free(connectionPorts)
sessionIdAllocator.free(connectionSessionId)
try {
// throws exception if this is not possible
connectionPorts = portAllocator.allocate(portsPerClient)
} catch (e: IllegalArgumentException) {
// have to unwind actions!
connectionsPerIpCounts.getAndDecrement(clientAddress)
logger.error("Unable to allocate a stream ID for the client connection!")
return
}
logger.error("Unable to allocate $portsPerClient ports for client connection!")
return
}
// allocate session/stream id's
val connectionSessionId: Int
try {
connectionSessionId = sessionIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
connectionsPerIpCounts.getAndDecrement(clientAddress)
portAllocator.free(connectionPorts)
logger.error("Unable to allocate a session ID for the client connection!")
return
}
val serverAddress = config.listenIpAddress // TODO :: my IP address?? this should be the IP of the box?
val subscriptionPort = connectionPorts[0]
val publicationPort = connectionPorts[1]
val connectionStreamId: Int
try {
connectionStreamId = streamIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
connectionsPerIpCounts.getAndDecrement(clientAddress)
portAllocator.free(connectionPorts)
sessionIdAllocator.free(connectionSessionId)
// create a new connection. The session ID is encrypted.
try {
// connection timeout of 0 doesn't matter. it is not used by the server
val clientConnection = UdpMediaDriverConnection(
serverAddress, subscriptionPort, publicationPort,
connectionStreamId, connectionSessionId, 0, message.isReliable)
logger.error("Unable to allocate a stream ID for the client connection!")
return
}
val connection: Connection = endPoint.newConnection(endPoint, clientConnection)
val serverAddress = config.listenIpAddress // TODO :: my IP address?? this should be the IP of the box?
val subscriptionPort = connectionPorts[0]
val publicationPort = connectionPorts[1]
// create a new connection. The session ID is encrypted.
try {
// connection timeout of 0 doesn't matter. it is not used by the server
val clientConnection = UdpMediaDriverConnection(
serverAddress, subscriptionPort, publicationPort,
connectionStreamId, connectionSessionId, 0, message.isReliable)
val connection: Connection = endPoint.newConnection(endPoint, clientConnection)
// VALIDATE:: are we allowed to connect to this server (now that we have the initial server information)
@Suppress("UNCHECKED_CAST")
val permitConnection = notifyFilter(connection as C)
if (!permitConnection) {
// have to unwind actions!
connectionsPerIpCounts.getAndDecrement(clientAddress)
portAllocator.free(connectionPorts)
sessionIdAllocator.free(connectionSessionId)
streamIdAllocator.free(connectionStreamId)
logger.error("Error creating new duologue")
notifyError(connection, ClientRejectedException("Connection was not permitted!"))
return
}
logger.info("Client connected [$clientAddressString:$subscriptionPort|$publicationPort] (session: $sessionId")
logger.debug("[{}] created new client connection", connectionSessionId)
// The one-time pad is used to encrypt the session ID, so that ONLY the correct client knows what it is!
val successMessage = Registration.helloAck(message.oneTimePad xor connectionSessionId)
successMessage.sessionId = sessionId // has to be the same as before (the client expects this)
successMessage.streamId = message.oneTimePad xor connectionStreamId
successMessage.subscriptionPort = subscriptionPort
successMessage.publicationPort = publicationPort
successMessage.publicKey = config.settingsStore.getPublicKey()
endPoint.writeMessage(handshakePublication, successMessage)
addConnection(connection)
notifyConnect(connection)
} catch (e: Exception) {
// VALIDATE:: are we allowed to connect to this server (now that we have the initial server information)
@Suppress("UNCHECKED_CAST")
val permitConnection = notifyFilter(connection as C)
if (!permitConnection) {
// have to unwind actions!
connectionsPerIpCounts.getAndDecrement(clientAddress)
portAllocator.free(connectionPorts)
@ -208,11 +184,37 @@ class ConnectionManagerServer<C : Connection>(logger: Logger,
logger.error("Error creating new duologue")
logger.error("could not process client message: $message")
notifyError(e)
notifyError(connection, ClientRejectedException("Connection was not permitted!"))
return
}
logger.info("Client connected [$clientAddressString:$subscriptionPort|$publicationPort] (session: $sessionId")
logger.debug("[{}] created new client connection", connectionSessionId)
// The one-time pad is used to encrypt the session ID, so that ONLY the correct client knows what it is!
val successMessage = Registration.helloAck(message.oneTimePad xor connectionSessionId)
successMessage.sessionId = sessionId // has to be the same as before (the client expects this)
successMessage.streamId = message.oneTimePad xor connectionStreamId
successMessage.subscriptionPort = subscriptionPort
successMessage.publicationPort = publicationPort
successMessage.publicKey = config.settingsStore.getPublicKey()
endPoint.writeHandshakeMessage(handshakePublication, successMessage)
addConnection(connection)
notifyConnect(connection)
} catch (e: Exception) {
logger.error("could not process client message: ", e)
// have to unwind actions!
connectionsPerIpCounts.getAndDecrement(clientAddress)
portAllocator.free(connectionPorts)
sessionIdAllocator.free(connectionSessionId)
streamIdAllocator.free(connectionStreamId)
logger.error("Error creating new duologue")
logger.error("could not process client message: $message")
notifyError(e)
}
}

View File

@ -15,7 +15,7 @@
*/
package dorkbox.network.connection
import dorkbox.network.rmi.ConnectionRmiSupport
import dorkbox.network.rmi.RmiSupportConnection
import javax.crypto.SecretKey
/**
@ -25,7 +25,7 @@ interface Connection_ : Connection {
/**
* @return the RMI support for this connection
*/
fun rmiSupport(): ConnectionRmiSupport
fun rmiSupport(): RmiSupportConnection<Connection_>
/**
* This is the per-message sequence number.

View File

@ -15,14 +15,17 @@
*/
package dorkbox.network.connection
import dorkbox.network.*
import dorkbox.network.Client
import dorkbox.network.Configuration
import dorkbox.network.Server
import dorkbox.network.ServerConfiguration
import dorkbox.network.aeron.CoroutineIdleStrategy
import dorkbox.network.connection.ping.PingMessage
import dorkbox.network.other.CryptoEccNative
import dorkbox.network.rmi.RmiServer
import dorkbox.network.other.NetworkUtil
import dorkbox.network.rmi.RmiSupport
import dorkbox.network.rmi.messages.RmiMessage
import dorkbox.network.serialization.NetworkSerializationManager
import dorkbox.network.serialization.Serialization
import dorkbox.network.store.NullSettingsStore
import dorkbox.network.store.SettingsStore
import dorkbox.util.NamedThreadFactory
@ -38,9 +41,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mu.KLogger
import mu.KotlinLogging
import org.agrona.DirectBuffer
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.security.KeyFactory
import java.security.PrivateKey
@ -58,7 +61,12 @@ import java.util.concurrent.CountDownLatch
// Usually it's with ISPs.
/**
* represents the base of a client/server end point for interacting with aeron
*/
*
* @param type this is either "Client" or "Server", depending on who is creating this endpoint.
* @param config these are the specific connection options
*
* @throws SecurityException if unable to initialize/generate ECC keys
*/
abstract class EndPoint<C : Connection>
internal constructor(val type: Class<*>, internal val config: Configuration) : AutoCloseable {
protected constructor(config: Configuration) : this(Client::class.java, config)
@ -81,7 +89,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
const val RESERVED_SESSION_ID_HIGH = Integer.MAX_VALUE
const val UDP_HANDSHAKE_STREAM_ID: Int = 0x1337cafe
const val IPC_STREAM_ID: Int = 0x1337c0de
const val IPC_HANDSHAKE_STREAM_ID_PUB: Int = 0x1337c0de
const val IPC_HANDSHAKE_STREAM_ID_SUB: Int = 0x1337c0d3
init {
println("THIS IS ONLY IPV4 AT THE MOMENT. IPV6 is in progress!")
@ -100,17 +109,16 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
}
// the simple name (for the class) of this connection endpoint
val name = type.simpleName
val logger: Logger = LoggerFactory.getLogger(name)
val logger: KLogger = KotlinLogging.logger(type.simpleName)
internal val closables = CopyOnWriteArrayList<AutoCloseable>()
internal val actionDispatch = CoroutineScope(Dispatchers.Default)
internal abstract val connectionManager: ConnectionManager<C>
internal lateinit var mediaDriver: MediaDriver
internal lateinit var aeron: Aeron
internal val mediaDriverContext: MediaDriver.Context
internal val mediaDriver: MediaDriver
internal val aeron: Aeron
/**
* Returns the serialization wrapper if there is an object type that needs to be added outside of the basics.
@ -130,9 +138,11 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
// we only want one instance of these created. These will be called appropriately
val settingsStore: SettingsStore
val rmiGlobalBridge = RmiServer(logger, true)
var disableRemoteKeyValidation = false
val rmiSupport = RmiSupport<C>(logger, actionDispatch, config.serialization)
/**
* Checks to see if this client has connected yet or not.
*
@ -142,14 +152,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
*/
abstract fun isConnected(): Boolean
/**
* @param type this is either "Client" or "Server", depending on who is creating this endpoint.
* @param config these are the specific connection options
*
* @throws SecurityException if unable to initialize/generate ECC keys
* @throws ServerException if unable to validate configuration
*
*/
init {
// Aeron configuration
@ -173,8 +176,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
// OS.isMacOsX() ->
// }
// val rmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.rmem_max")
// val wmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.wmem_max")
// val rmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.rmem_max")
// val wmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.wmem_max")
}
@ -186,8 +189,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
// OS.isMacOsX() ->
// }
// val rmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.rmem_max")
// val wmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.wmem_max")
// val rmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.rmem_max")
// val wmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.wmem_max")
}
@ -230,63 +233,58 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
val aeronLogDirectory = File(baseFile, baseName)
if (aeronLogDirectory.exists()) {
logger.error("Aeron log directory already exists! This might not be what you want!")
// avoid a collision
// aeronLogDirectory = File(baseFile, baseName + RandomUtil.get().nextInt(1000))
}
logger.debug("Aeron log directory: $aeronLogDirectory")
config.aeronLogDirectory = aeronLogDirectory
}
// the RmiNoOpConnection must have an endpoint, and we DO NOT want it to actually setup/configure aeron!
if (config.publicationPort > 0) {
val threadFactory = NamedThreadFactory("Aeron", false)
val threadFactory = NamedThreadFactory("Aeron", false)
// LOW-LATENCY SETTINGS
// .termBufferSparseFile(false)
// .useWindowsHighResTimer(true)
// .threadingMode(ThreadingMode.DEDICATED)
// .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE)
// .receiverIdleStrategy(NoOpIdleStrategy.INSTANCE)
// .senderIdleStrategy(NoOpIdleStrategy.INSTANCE);
val mediaDriverContext = MediaDriver.Context()
.publicationReservedSessionIdLow(RESERVED_SESSION_ID_LOW)
.publicationReservedSessionIdHigh(RESERVED_SESSION_ID_HIGH)
.dirDeleteOnStart(true) // TODO: FOR NOW?
.dirDeleteOnShutdown(true)
.conductorThreadFactory(threadFactory)
.receiverThreadFactory(threadFactory)
.senderThreadFactory(threadFactory)
.sharedNetworkThreadFactory(threadFactory)
.sharedThreadFactory(threadFactory)
.threadingMode(config.threadingMode)
.mtuLength(config.networkMtuSize)
.socketSndbufLength(config.sendBufferSize)
.socketRcvbufLength(config.receiveBufferSize)
.aeronDirectoryName(config.aeronLogDirectory!!.absolutePath)
// LOW-LATENCY SETTINGS
// .termBufferSparseFile(false)
// .useWindowsHighResTimer(true)
// .threadingMode(ThreadingMode.DEDICATED)
// .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE)
// .receiverIdleStrategy(NoOpIdleStrategy.INSTANCE)
// .senderIdleStrategy(NoOpIdleStrategy.INSTANCE);
mediaDriverContext = MediaDriver.Context()
.publicationReservedSessionIdLow(RESERVED_SESSION_ID_LOW)
.publicationReservedSessionIdHigh(RESERVED_SESSION_ID_HIGH)
.dirDeleteOnStart(true) // TODO: FOR NOW?
.dirDeleteOnShutdown(true)
.conductorThreadFactory(threadFactory)
.receiverThreadFactory(threadFactory)
.senderThreadFactory(threadFactory)
.sharedNetworkThreadFactory(threadFactory)
.sharedThreadFactory(threadFactory)
.threadingMode(config.threadingMode)
.mtuLength(config.networkMtuSize)
.socketSndbufLength(config.sendBufferSize)
.socketRcvbufLength(config.receiveBufferSize)
.aeronDirectoryName(config.aeronLogDirectory!!.absolutePath)
val aeronContext = Aeron.Context().aeronDirectoryName(mediaDriverContext.aeronDirectoryName())
val aeronContext = Aeron.Context().aeronDirectoryName(mediaDriverContext.aeronDirectoryName())
try {
mediaDriver = MediaDriver.launch(mediaDriverContext)
} catch (e: Exception) {
throw e
}
try {
aeron = Aeron.connect(aeronContext)
} catch (e: Exception) {
try {
mediaDriver.close()
} catch (secondaryException: Exception) {
e.addSuppressed(secondaryException)
}
throw e
}
closables.add(aeron)
closables.add(mediaDriver)
try {
mediaDriver = MediaDriver.launch(mediaDriverContext)
} catch (e: Exception) {
throw e
}
try {
aeron = Aeron.connect(aeronContext)
} catch (e: Exception) {
try {
mediaDriver.close()
} catch (secondaryException: Exception) {
e.addSuppressed(secondaryException)
}
throw e
}
closables.add(aeron)
closables.add(mediaDriver)
// serialization stuff
serialization = config.serialization
@ -417,6 +415,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
*/
@Suppress("MemberVisibilityCanBePrivate")
open fun newConnection(endPoint: EndPoint<C>, mediaDriverConnection: MediaDriverConnection): C {
@Suppress("UNCHECKED_CAST")
return ConnectionImpl(endPoint, mediaDriverConnection) as C
}
@ -498,56 +497,13 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
connectionManager.forEachConnectionDoRead(function)
}
/**
* Creates a "global" RMI object for use by multiple connections.
*
* @return the ID assigned to this RMI object
*/
fun <T> createGlobalObject(globalObject: T): Int {
return rmiGlobalBridge.register(globalObject) ?: 0
}
/**
* Gets a previously created "global" RMI object
*
* @param objectRmiId the ID of the RMI object to get
*
* @return null if the object doesn't exist or the ID is invalid.
*/
fun <T> getGlobalObject(objectRmiId: Int): T {
@Suppress("UNCHECKED_CAST")
return rmiGlobalBridge.getRegisteredObject(objectRmiId) as T
}
suspend fun writeMessage(publication: Publication, message: Any) {
writeMessage(publication, Serialization.NOP_CONNECTION, message)
}
suspend fun writeMessage(publication: Publication, connection: Connection_, message: Any) {
sendIdleStrategy.reset()
// TODO: WE MIGHT NOT WANT TO USE SESSIONID()!!
internal suspend fun writeHandshakeMessage(publication: Publication, message: Any) {
// The sessionId is globally unique, and is assigned by the server.
logger.debug("[{}] send: {}", publication.sessionId(), message)
val kryo: KryoExtra = serialization.takeKryo()
try {
kryo.write(connection, message)
kryo.write(message)
val buffer = kryo.writerBuffer
val objectSize = buffer.position()
@ -569,28 +525,17 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
// more critical error sending the message. we shouldn't retry or anything.
logger.error("Error sending message. ${errorCodeName(result)}")
return
}
} catch (e: Exception) {
logger.error("Error serializing message $message", e)
} finally {
sendIdleStrategy.reset()
serialization.returnKryo(kryo)
}
}
/**
* @param buffer The buffer
* @param offset The offset from the start of the buffer
@ -598,14 +543,12 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
*
* @return A string
*/
fun readHandshakeMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header): Any? {
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
// val sessionId = header.sessionId()
// TODO: WE MIGHT NOT WANT TO USE SESSIONID()!!
internal fun readHandshakeMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header): Any? {
val kryo: KryoExtra = serialization.takeKryo()
try {
return kryo.read(buffer, offset, length, Serialization.NOP_CONNECTION)
val message = kryo.read(buffer, offset, length)
logger.debug("[{}] received: {}", header.sessionId(), message)
return message
} catch (e: Exception) {
logger.error("Error de-serializing message on connection ${header.sessionId()}!", e)
} finally {
@ -616,7 +559,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
}
suspend fun readMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header, connection: Connection_) {
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
// The sessionId is globally unique, and is assigned by the server.
val sessionId = header.sessionId()
// note: this address will ALWAYS be an IP:PORT combo
@ -630,7 +573,6 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
// val ipAsInt = NetworkUtil.IP.toInt(ip)
// TODO: WE MIGHT NOT WANT TO USE SESSIONID()!!
var message: Any? = null
val kryo: KryoExtra = serialization.takeKryo()
@ -638,7 +580,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
message = kryo.read(buffer, offset, length, connection)
logger.debug("[{}] received: {}", sessionId, message)
} catch (e: Exception) {
logger.error("[${header.sessionId()}] Error de-serializing message", e)
logger.error("[${sessionId}] Error de-serializing message", e)
} finally {
serialization.returnKryo(kryo)
}
@ -647,7 +589,6 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
val data = ByteArray(length)
buffer.getBytes(offset, data)
when (message) {
is PingMessage -> {
// the ping listener (internal use only!)
@ -664,8 +605,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
is RmiMessage -> {
// if we are an RMI message/registration, we have very specific, defined behavior.
// We do not use the "normal" listener callback pattern because this require special functionality
// note: RMI messages are NEVER subclassed!
connection.rmiSupport().manage(connection, message, logger)
@Suppress("UNCHECKED_CAST")
rmiSupport.manage(this as EndPoint<Connection_>, connection, message, logger)
}
is Any -> {
connectionManager.notifyOnMessage(connection, message)
@ -697,7 +638,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
override fun toString(): String {
return "EndPoint [$name]"
return "EndPoint [${type.simpleName}]"
}
override fun hashCode(): Int {

View File

@ -1,65 +0,0 @@
package dorkbox.network.connection
import dorkbox.network.aeron.client.ClientTimedOutException
import io.aeron.Aeron
import io.aeron.ChannelUriStringBuilder
import io.aeron.Publication
import io.aeron.Subscription
class IpcMediaDriverConnection(override val address: String,
override val subscriptionPort: Int,
override val publicationPort: Int,
override val streamId: Int,
override val sessionId: Int,
private val connectionTimeoutMS: Long,
override val isReliable: Boolean) : MediaDriverConnection {
override lateinit var subscription: Subscription
override lateinit var publication: Publication
init {
}
fun subscriptionURI(): ChannelUriStringBuilder {
return ChannelUriStringBuilder()
.media("ipc")
.controlMode("dynamic")
}
// Create a publication at the given address and port, using the given stream ID.
fun publicationURI(): ChannelUriStringBuilder {
return ChannelUriStringBuilder()
.media("ipc")
}
@Throws(ClientTimedOutException::class)
override suspend fun buildClient(aeron: Aeron) {
}
override fun buildServer(aeron: Aeron) {
}
override fun clientInfo() : String {
return ""
}
override fun serverInfo() : String {
return ""
}
fun connect() : Pair<String, String> {
return Pair("","")
}
override fun close() {
}
override fun toString(): String {
return "$address [$subscriptionPort|$publicationPort] [$streamId|$sessionId]"
}
}

View File

@ -20,8 +20,7 @@ import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.pipeline.AeronInput
import dorkbox.network.pipeline.AeronOutput
import dorkbox.network.rmi.ConnectionRmiSupport
import dorkbox.network.serialization.NetworkSerializationManager
import dorkbox.network.rmi.CachedMethod
import dorkbox.util.OS
import dorkbox.util.bytes.OptimizeUtilsByteArray
import dorkbox.util.bytes.OptimizeUtilsByteBuf
@ -29,15 +28,15 @@ import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil
import net.jpountz.lz4.LZ4Factory
import org.agrona.DirectBuffer
import org.agrona.collections.Int2ObjectHashMap
import org.slf4j.Logger
import java.io.IOException
import java.security.SecureRandom
import javax.crypto.Cipher
/**
* Nothing in this class is thread safe
*/
class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() {
class KryoExtra(private val methodCache: Int2ObjectHashMap<Array<CachedMethod>>) : Kryo() {
// for kryo serialization
private val readerBuffer = AeronInput()
val writerBuffer = AeronOutput()
@ -50,10 +49,9 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
// volatile to provide object visibility for entire class. This is unique per connection
lateinit var rmiSupport: ConnectionRmiSupport
lateinit var connection: Connection_
private val secureRandom = SecureRandom()
// private val secureRandom = SecureRandom()
private var cipher: Cipher? = null
private val compressor = factory.fastCompressor()
private val decompressor = factory.fastDecompressor()
@ -81,6 +79,24 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
}
}
fun getMethods(classId: Int): Array<CachedMethod> {
return methodCache[classId]
}
/**
* NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI!
*
* OUTPUT:
* ++++++++++++++++++++++++++
* + class and object bytes +
* ++++++++++++++++++++++++++
*/
@Throws(Exception::class)
fun write(message: Any) {
writerBuffer.reset()
writeClassAndObject(writerBuffer, message)
}
/**
* OUTPUT:
* ++++++++++++++++++++++++++
@ -91,12 +107,27 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
fun write(connection: Connection_, message: Any) {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
this.connection = connection
this.rmiSupport = connection.rmiSupport()
writerBuffer.reset()
writeClassAndObject(writerBuffer, message)
}
/**
* NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI!
*
* INPUT:
* ++++++++++++++++++++++++++
* + class and object bytes +
* ++++++++++++++++++++++++++
*/
@Throws(Exception::class)
fun read(buffer: DirectBuffer, offset: Int, length: Int): Any {
// this properly sets the buffer info
readerBuffer.setBuffer(buffer, offset, length)
return readClassAndObject(readerBuffer)
}
/**
* INPUT:
* ++++++++++++++++++++++++++
@ -107,7 +138,6 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
fun read(buffer: DirectBuffer, offset: Int, length: Int, connection: Connection_): Any {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
this.connection = connection
this.rmiSupport = connection.rmiSupport()
// this properly sets the buffer info
readerBuffer.setBuffer(buffer, offset, length)
@ -124,6 +154,20 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
////////////////
////////////////
/**
* NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI!
*
* OUTPUT:
* ++++++++++++++++++++++++++
* + class and object bytes +
* ++++++++++++++++++++++++++
*/
private fun write(writer: Output, message: Any) {
// write the object to the NORMAL output buffer!
writer.reset()
writeClassAndObject(writer, message)
}
/**
* OUTPUT:
* ++++++++++++++++++++++++++
@ -133,13 +177,24 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
private fun write(connection: Connection_, writer: Output, message: Any) {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
this.connection = connection
this.rmiSupport = connection.rmiSupport()
// write the object to the NORMAL output buffer!
writer.reset()
writeClassAndObject(writer, message)
}
/**
* NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI!
*
* INPUT:
* ++++++++++++++++++++++++++
* + class and object bytes +
* ++++++++++++++++++++++++++
*/
private fun read(reader: Input): Any {
return readClassAndObject(reader)
}
/**
* INPUT:
* ++++++++++++++++++++++++++
@ -149,11 +204,54 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
private fun read(connection: Connection_, reader: Input): Any {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
this.connection = connection
this.rmiSupport = connection.rmiSupport()
return readClassAndObject(reader)
}
/**
* NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI!
*
* BUFFER:
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* + uncompressed length (1-4 bytes) + compressed data +
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*
* COMPRESSED DATA:
* ++++++++++++++++++++++++++
* + class and object bytes +
* ++++++++++++++++++++++++++
*/
fun writeCompressed(logger: Logger, buffer: ByteBuf, message: Any) {
// write the object to a TEMP buffer! this will be compressed later
write(writer, message)
// save off how much data the object took
val length = writer.position()
val maxCompressedLength = compressor.maxCompressedLength(length)
////////// compressing data
// we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger
// output), will be negated by the increase in size by the encryption
val compressOutput = temp
// LZ4 compress.
val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 0, maxCompressedLength)
if (DEBUG) {
val orig = ByteBufUtil.hexDump(writer.buffer, 0, length)
val compressed = ByteBufUtil.hexDump(compressOutput, 0, compressedLength)
logger.error(OS.LINE_SEPARATOR +
"ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig +
OS.LINE_SEPARATOR +
"COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed)
}
// now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version
OptimizeUtilsByteBuf.writeInt(buffer, length, true)
// 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(compressOutput, 0, compressedLength)
}
/**
* BUFFER:
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
@ -196,6 +294,60 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
buffer.writeBytes(compressOutput, 0, compressedLength)
}
/**
* NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI!
*
* BUFFER:
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* + uncompressed length (1-4 bytes) + compressed data +
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*
* COMPRESSED DATA:
* ++++++++++++++++++++++++++
* + class and object bytes +
* ++++++++++++++++++++++++++
*/
fun readCompressed(logger: Logger, buffer: ByteBuf, length: Int): Any {
////////////////
// Note: we CANNOT write BACK to the buffer as "temp" storage, since there could be additional data on it!
////////////////
// get the decompressed length (at the beginning of the array)
var length = length
val uncompressedLength = OptimizeUtilsByteBuf.readInt(buffer, true)
if (uncompressedLength > ABSOLUTE_MAX_SIZE_OBJECT) {
throw IOException("Uncompressed size ($uncompressedLength) is larger than max allowed size ($ABSOLUTE_MAX_SIZE_OBJECT)!")
}
// because 1-4 bytes for the decompressed size (this number is never negative)
val lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true)
val start = buffer.readerIndex()
// have to adjust for uncompressed length-length
length = length - lengthLength
///////// decompress data
buffer.readBytes(temp, 0, length)
// LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor)
reader.reset()
decompressor.decompress(temp, 0, reader.buffer, 0, uncompressedLength)
reader.setLimit(uncompressedLength)
if (DEBUG) {
val compressed = ByteBufUtil.hexDump(buffer, start, length)
val orig = ByteBufUtil.hexDump(reader.buffer, start, uncompressedLength)
logger.error(OS.LINE_SEPARATOR +
"COMPRESSED: (" + length + ")" + OS.LINE_SEPARATOR + compressed +
OS.LINE_SEPARATOR +
"ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig)
}
// read the object from the buffer.
return read(reader)
}
/**
* BUFFER:
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

View File

@ -17,22 +17,6 @@ package dorkbox.network.connection
interface Listener {}
/**
* Called before the remote end has been connected.
* <p>
* This permits the addition of connection filters to decide if a connection is permitted.
*/
interface FilterConnection<C : Connection> {
/**
* Called before the remote end has been connected.
* <p>
* This permits the addition of connection filters to decide if a connection is permitted.
* <p>
* @return true if the connection is permitted, false if it will be rejected
*/
fun filter(connection: C): Boolean
}
/**
* Called when the remote end has been connected. This will be invoked before any objects are received by the network.
*/
@ -43,51 +27,4 @@ interface OnConnected<C : Connection> {
fun connected(connection: C)
}
/**
* Called when the remote end is no longer connected.
* <p>
* Do not try to send messages! The connection will already be closed, resulting in an error if you attempt to do so.
*/
interface OnDisconnected<C : Connection> {
/**
* Called when the remote end is no longer connected.
* <p>
* Do not write data in this method! The connection can already be closed, resulting in an error if you attempt to do so.
*/
fun disconnected(connection: C)
}
/**
* Called when there is an error
* <p>
* The error is also sent to an error log before this method is called.
*/
interface OnError<C : Connection> {
/**
* Called when there is an error
* <p>
* The error is sent to an error log before this method is called.
*/
fun error(connection: C, throwable: Throwable)
}
/**
* Called when an object has been received from the remote end of the connection.
* <p>
* This method should not block for long periods as other network activity will not be processed until it returns.
*/
interface OnMessageReceived<C : Connection, M : Any> {
fun received(connection: C, message: M)
}
/**
* Permits a listener to specify it's own referenced object type, if passing in a generic parameter doesn't work. This is necessary since
* the system looks up incoming message types to determine what listeners to dispatch them to.
*/
interface SelfDefinedType {
/**
* Permits a listener to specify it's own referenced object type, if passing in a generic parameter doesn't work. This is necessary since
* the system looks up incoming message types to determine what listeners to dispatch them to.
*/
fun type(): Class<*>
}

View File

@ -20,7 +20,6 @@ package dorkbox.network.connection
* accidentally add an incompatible connection type.
*/
interface Listeners<C> where C : Connection {
/**
* Adds a function that will be called BEFORE a client/server "connects" with
* each other, and used to determine if a connection should be allowed

View File

@ -35,12 +35,12 @@ interface MediaDriverConnection : AutoCloseable {
* For a client, the ports specified here MUST be manually flipped because they are in the perspective of the SERVER
*/
class UdpMediaDriverConnection(override val address: String,
override val subscriptionPort: Int,
override val publicationPort: Int,
override val streamId: Int,
override val sessionId: Int,
private val connectionTimeoutMS: Long = 0,
override val isReliable: Boolean = true) : MediaDriverConnection {
override val subscriptionPort: Int,
override val publicationPort: Int,
override val streamId: Int,
override val sessionId: Int,
private val connectionTimeoutMS: Long = 0,
override val isReliable: Boolean = true) : MediaDriverConnection {
override lateinit var subscription: Subscription
override lateinit var publication: Publication
@ -174,3 +174,137 @@ class UdpMediaDriverConnection(override val address: String,
return "$address [$subscriptionPort|$publicationPort] [$streamId|$sessionId] (reliable:$isReliable)"
}
}
/**
* For a client, the streamId specified here MUST be manually flipped because they are in the perspective of the SERVER
*/
class IpcMediaDriverConnection(override val streamId: Int,
val streamIdSubscription: Int,
override val sessionId: Int,
private val connectionTimeoutMS: Long = 30_000,
override val isReliable: Boolean = true) : MediaDriverConnection {
override val address = ""
override val subscriptionPort = 0
override val publicationPort = 0
override lateinit var subscription: Subscription
override lateinit var publication: Publication
var success: Boolean = false
init {
}
private fun uri(): ChannelUriStringBuilder {
val builder = ChannelUriStringBuilder().media("ipc")
if (sessionId != EndPoint.RESERVED_SESSION_ID_INVALID) {
builder.sessionId(sessionId)
}
return builder
}
@Throws(ClientTimedOutException::class)
override suspend fun buildClient(aeron: Aeron) {
// Create a subscription with a control port (for dynamic MDC) at the given address and port, using the given stream ID.
val subscriptionUri = uri()
// .controlEndpoint("$address:$subscriptionPort")
// .controlMode("dynamic")
// Create a publication at the given address and port, using the given stream ID.
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
val publicationUri = uri()
// .endpoint("$address:$publicationPort")
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
val subscription = aeron.addSubscription(subscriptionUri.build(), streamIdSubscription)
val publication = aeron.addPublication(publicationUri.build(), streamId)
var success = false
// this will wait for the server to acknowledge the connection (all via aeron)
var startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < connectionTimeoutMS) {
if (subscription.isConnected && subscription.imageCount() > 0) {
success = true
break
}
delay(timeMillis = 10L)
}
if (!success) {
subscription.close()
throw ClientTimedOutException("Creating subscription connection to aeron")
}
success = false
// this will wait for the server to acknowledge the connection (all via aeron)
startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < connectionTimeoutMS) {
if (publication.isConnected) {
success = true
break
}
delay(timeMillis = 10L)
}
if (!success) {
subscription.close()
publication.close()
throw ClientTimedOutException("Creating publication connection to aeron")
}
this.success = true
this.subscription = subscription
this.publication = publication
}
override fun buildServer(aeron: Aeron) {
// Create a subscription with a control port (for dynamic MDC) at the given address and port, using the given stream ID.
val subscriptionUri = uri()
// .endpoint("$address:$subscriptionPort")
// Create a publication with a control port (for dynamic MDC) at the given address and port, using the given stream ID.
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
val publicationUri = uri()
// .controlEndpoint("$address:$publicationPort")
// .controlMode("dynamic")
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
subscription = aeron.addSubscription(subscriptionUri.build(), streamIdSubscription)
publication = aeron.addPublication(publicationUri.build(), streamId)
}
override fun clientInfo() : String {
return ""
}
override fun serverInfo() : String {
return ""
}
fun connect() : Pair<String, String> {
return Pair("","")
}
override fun close() {
}
override fun toString(): String {
return "$address [$subscriptionPort|$publicationPort] [$streamId|$sessionId]"
}
}

View File

@ -25,11 +25,13 @@ class Registration private constructor() {
// -1 means there is an error
var state = INVALID
var errorMessage: String? = null
var publicationPort = 0
var subscriptionPort = 0
var sessionId = 0
var streamId = 0
var publicKey: ByteArray? = null
// by default, this will be a reliable connection. When the client connects to the server, the client will specify if the new connection
@ -37,9 +39,12 @@ class Registration private constructor() {
val isReliable = true
// the client sends it's registration data to the server to make sure that the registered classes are the same between the client/server
var registrationData: ByteArray? = null
// NOTE: this is for ECDSA!
// var eccParameters: IESParameters? = null
var payload: ByteArray? = null
// > 0 when we are ready to setup the connection (hasMore will always be false if this is >0). 0 when we are ready to connect
// ALSO used if there are fragmented frames for registration data (since we have to split it up to fit inside a single UDP packet without fragmentation)
@ -53,11 +58,12 @@ class Registration private constructor() {
const val HELLO = 0
const val HELLO_ACK = 1
fun hello(oneTimePad: Int, publicKey: ByteArray): Registration {
fun hello(oneTimePad: Int, publicKey: ByteArray, registrationData: ByteArray): Registration {
val hello = Registration()
hello.state = HELLO
hello.oneTimePad = oneTimePad
hello.publicKey = publicKey
hello.registrationData = registrationData
return hello
}

View File

@ -0,0 +1,34 @@
package dorkbox.network.other
import java.lang.reflect.InvocationTargetException
import kotlin.coroutines.Continuation
@FunctionalInterface
private interface SuspendFunction {
suspend fun invoke(): Any?
}
// we access this via reflection, because we have to be able to pass the continuation object as the LAST parameter. We don't know what
// the method signature actually is, so this is necessary
private val SuspendMethod = SuspendFunction::class.java.methods[0]
internal inline fun handleInvocationTargetException(action: () -> Any?): Any? {
return try {
action()
} catch (e: InvocationTargetException) {
throw e.cause!!
}
}
internal fun invokeSuspendFunction(continuation: Continuation<*>, suspendFunction: suspend () -> Any?): Any? {
return handleInvocationTargetException {
SuspendMethod.invoke(
object : SuspendFunction {
override suspend fun invoke() = suspendFunction()
},
continuation
)
}
}

View File

@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
package dorkbox.network;
package dorkbox.network.other;
import static io.netty.util.AsciiString.indexOf;

View File

@ -1,4 +1,4 @@
package dorkbox.network;
package dorkbox.network.other;
import java.io.BufferedWriter;
import java.io.File;

View File

@ -0,0 +1,14 @@
package dorkbox.network.other
import kotlinx.coroutines.channels.Channel
// this is bi-directional waiting. The method names to not reflect this, however there is no possibility of race conditions w.r.t. waiting
// https://kotlinlang.org/docs/reference/coroutines/channels.html
inline class SuspendWaiter(private val channel: Channel<Unit> = Channel(0)) {
// "receive' suspends until another coroutine invokes "send"
// and
// "send" suspends until another coroutine invokes "receive".
suspend fun doWait() { channel.receive() }
suspend fun doNotify() { channel.send(Unit) }
fun cancel() { channel.cancel() }
}

View File

@ -39,7 +39,7 @@ import dorkbox.network.connection.Connection
import java.lang.reflect.Method
/**
* This class is NOT sent across the wire
* This class is NOT sent across the wire, but some of it's contents are
*/
open class CachedMethod(val method: Method, val methodIndex: Int, val methodClassId: Int, val serializers: Array<Serializer<*>?>) {
/**

View File

@ -1,361 +0,0 @@
/*
* Copyright 2019 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.rmi
import dorkbox.network.connection.*
import dorkbox.network.rmi.messages.DynamicObjectRequest
import dorkbox.network.rmi.messages.DynamicObjectResponse
import dorkbox.network.rmi.messages.MethodRequest
import dorkbox.network.rmi.messages.MethodResponse
import dorkbox.network.serialization.NetworkSerializationManager
import dorkbox.util.classes.ClassHelper
import dorkbox.util.collections.LockFreeHashMap
import dorkbox.util.collections.LockFreeIntMap
import org.slf4j.Logger
import java.io.IOException
import java.lang.reflect.Proxy
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
class ConnectionRmiSupport internal constructor(private val rmiGlobal: RmiServer) {
private val rmiLocal: RmiServer
private val proxyIdCache: MutableMap<Int, RemoteObject?>
private val proxyListeners: MutableList<OnMessageReceived<Connection, MethodResponse>>
private val rmiRegistrationCallbacks: LockFreeIntMap<RemoteObjectCallback<*>>
@Volatile
private var rmiCallbackId = 0
init {
// * @param executor
// * Sets the executor used to invoke methods when an invocation is received from a remote endpoint. By default, no
// * executor is set and invocations occur on the network thread, which should not be blocked for long, May be null.
rmiLocal = RmiServer(rmiGlobal.logger, false)
proxyIdCache = LockFreeHashMap()
proxyListeners = CopyOnWriteArrayList()
rmiRegistrationCallbacks = LockFreeIntMap()
}
fun close() {
// proxy listeners are cleared in the removeAll() call (which happens BEFORE close)
proxyIdCache.clear()
rmiRegistrationCallbacks.clear()
}
/**
* This will remove the invoke and invoke response listeners for this remote object
*/
fun removeAllListeners() {
proxyListeners.clear()
}
suspend fun <Iface> createRemoteObject(
connection: ConnectionImpl,
interfaceClass: Class<Iface>,
callback: RemoteObjectCallback<Iface>) {
require(interfaceClass.isInterface) { "Cannot create a proxy for RMI access. It must be an interface." }
// because this is PER CONNECTION, there is no need for synchronize(), since there will not be any issues with concurrent
// access, but there WILL be issues with thread visibility because a different worker thread can be called for different connections
val nextRmiCallbackId = rmiCallbackId++
rmiRegistrationCallbacks.put(nextRmiCallbackId, callback)
val message = DynamicObjectRequest(interfaceClass, RmiServer.INVALID_RMI, nextRmiCallbackId)
// We use a callback to notify us when the object is ready. We can't "create this on the fly" because we
// 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
connection.send(message)
}
suspend fun <Iface> getRemoteObject(connection: ConnectionImpl, objectId: Int, callback: RemoteObjectCallback<Iface>) {
check(objectId >= 0) { "Object ID cannot be < 0" }
check(objectId < RmiServer.INVALID_RMI) { "Object ID cannot be >= " + RmiServer.INVALID_RMI }
val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(RemoteObjectCallback::class.java, callback.javaClass, 0)
// because this is PER CONNECTION, there is no need for synchronize(), since there will not be any issues with concurrent
// access, but there WILL be issues with thread visibility because a different worker thread can be called for different connections
val nextRmiCallbackId = rmiCallbackId++
rmiRegistrationCallbacks.put(nextRmiCallbackId, callback)
val message = DynamicObjectRequest(iFaceClass, objectId, nextRmiCallbackId)
// We use a callback to notify us when the object is ready. We can't "create this on the fly" because we
// 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 getting an EXISTING object on the server, bound access to only this connection
connection.send(message)
}
/**
* Manages the RMI stuff for a connection. Will register/invoke/etc on the RMI object
*/
suspend fun manage(connection: Connection_, message: Any, logger: Logger) {
when (message) {
is MethodRequest -> {
val objectID = message.objectId
// have to make sure to get the correct object (global vs local)
// This is what is overridden when registering interfaces/classes for RMI.
// objectID is the interface ID, and this returns the implementation ID.
val target = getImplementationObject(objectID)
if (target == null) {
logger.warn("Ignoring remote invocation request for unknown object ID: {}", objectID)
return
}
try {
val result = RmiServer.invoke(connection, target, message, logger)
if (result != null) {
// System.err.println("Sending: " + invokeMethod.responseID);
connection.send(result)
}
} catch (e: IOException) {
logger.error("Unable to invoke method.", e)
}
}
is MethodResponse -> {
for (proxyListener in proxyListeners) {
proxyListener.received(connection, message)
}
}
is DynamicObjectRequest -> {
// Check if we are creating a new REMOTE object. This check is always first.
if (message.rmiId == RmiServer.INVALID_RMI) {
// THIS IS ON THE REMOTE CONNECTION (where the object will really exist as an implementation)
//
// CREATE a new ID, and register the ID and new object (must create a new one) in the object maps
// have to lookup the implementation class
val serialization = connection.endPoint().serialization
// For network connections, the interface class kryo ID == implementation class kryo ID, so they switch automatically.
val registrationResult = createNewRmiObject(serialization, message.interfaceClass, message.callbackId, logger)
connection.send(registrationResult)
// connection transport is flushed in calling method (don't need to do it here)
} else {
// THIS IS ON THE REMOTE CONNECTION (where the object implementation will really exist)
//
// GET a LOCAL rmi object, if none get a specific, GLOBAL rmi object (objects that are not bound to a single connection).
val implementationObject = getImplementationObject(message.rmiId)
connection.send(DynamicObjectResponse(message.interfaceClass, message.rmiId, message.callbackId, implementationObject!!))
// connection transport is flushed in calling method (don't need to do it here)
}
}
is DynamicObjectResponse -> {
if (message.rmiId == RmiServer.INVALID_RMI) {
logger.error("RMI ID '{}' is invalid. Unable to create RMI object.", message.rmiId)
}
// this is the response.
// THIS IS ON THE LOCAL CONNECTION SIDE, which is the side that called 'getRemoteObject()'
@Suppress("UNCHECKED_CAST")
val callback: RemoteObjectCallback<Any> = rmiRegistrationCallbacks.remove(message.callbackId) as RemoteObjectCallback<Any>
try {
callback.created(message.remoteObject!!)
} catch (e: Exception) {
logger.error("Error getting or creating the remote object ${message.interfaceClass}", e)
}
}
}
}
/**
* Used by RMI by the LOCAL side when setting up the to fetch an object for the REMOTE side
*
* @return the registered ID for a specific object, or RmiBridge.INVALID_RMI if there was no ID.
*/
fun <T> getRegisteredId(`object`: T): Int {
// always check global before checking local, because less contention on the synchronization
val objectId = rmiGlobal.getRegisteredId(`object`)
return if (objectId != RmiServer.INVALID_RMI) {
objectId
} else {
// might return RmiBridge.INVALID_RMI;
rmiLocal.getRegisteredId(`object`)
}
}
/**
* This is used by RMI for the SERVER side, to get the implementation
*
* @param objectId this is the RMI object ID
*/
fun getImplementationObject(objectId: Int): Any? {
return if (RmiServer.isGlobal(objectId)) {
rmiGlobal.getRegisteredObject(objectId)
} else {
rmiLocal.getRegisteredObject(objectId)
}
}
/**
* Removes a proxy object from the system
*/
fun removeProxyObject(rmiProxyHandler: RmiClient) {
proxyListeners.remove(rmiProxyHandler.listener)
proxyIdCache.remove(rmiProxyHandler.rmiObjectId)
}
/**
* For network connections, the interface class kryo ID == implementation class kryo ID, so they switch automatically.
* For local connections, we have to switch it appropriately in the LocalRmiProxy
*/
fun createNewRmiObject(serialization: NetworkSerializationManager, interfaceClass: Class<*>, callbackId: Int, logger: Logger): DynamicObjectResponse {
var kryo: KryoExtra? = null
var remoteObject: Any? = null
var rmiId = 0
val implementationClass = serialization.getRmiImpl(interfaceClass)
try {
kryo = serialization.takeKryo()
// because the INTERFACE is what is registered with kryo (not the impl) we have to temporarily permit unregistered classes (which have an ID of -1)
// so we can cache the instantiator for this class.
val registrationRequired = kryo.isRegistrationRequired
kryo.isRegistrationRequired = false
// this is what creates a new instance of the impl class, and stores it as an ID.
remoteObject = kryo.newInstance(implementationClass)
if (registrationRequired) {
// only if it's different should we call this again.
kryo.isRegistrationRequired = true
}
rmiId = rmiLocal.register(remoteObject)
if (rmiId == RmiServer.INVALID_RMI) {
// this means that there are too many RMI ids (either global or connection specific!)
remoteObject = null
} else {
// if we are invalid, skip going over fields that might also be RMI objects, BECAUSE our object will be NULL!
// the @Rmi annotation allows an RMI object to have fields with objects that are ALSO RMI objects
val classesToCheck = LinkedList<Map.Entry<Class<*>, Any?>>()
classesToCheck.add(AbstractMap.SimpleEntry(implementationClass, remoteObject))
var remoteClassObject: Map.Entry<Class<*>, Any?>
while (!classesToCheck.isEmpty()) {
remoteClassObject = classesToCheck.removeFirst()
// we have to check the IMPLEMENTATION for any additional fields that will have proxy information.
// we use getDeclaredFields() + walking the object hierarchy, so we get ALL the fields possible (public + private).
for (field in remoteClassObject.key.declaredFields) {
if (field.getAnnotation(Rmi::class.java) != null) {
val type = field.type
if (!type.isInterface) {
// the type must be an interface, otherwise RMI cannot create a proxy object
logger.error("Error checking RMI fields for: {}.{} -- It is not an interface!",
remoteClassObject.key,
field.name)
continue
}
val prev = field.isAccessible
field.isAccessible = true
val o: Any
try {
o = field[remoteClassObject.value]
rmiLocal.register(o)
classesToCheck.add(AbstractMap.SimpleEntry(type, o))
} catch (e: IllegalAccessException) {
logger.error("Error checking RMI fields for: {}.{}", remoteClassObject.key, field.name, e)
} finally {
field.isAccessible = prev
}
}
}
// have to check the object hierarchy as well
val superclass = remoteClassObject.key
.superclass
if (superclass != null && superclass != Any::class.java) {
classesToCheck.add(AbstractMap.SimpleEntry(superclass, remoteClassObject.value))
}
}
}
} catch (e: Exception) {
logger.error("Error registering RMI class $implementationClass", e)
} finally {
if (kryo != null) {
// we use kryo to create a new instance - so only return it on error or when it's done creating a new instance
serialization.returnKryo(kryo)
}
}
return DynamicObjectResponse(interfaceClass, rmiId, callbackId, remoteObject!!)
}
/**
* Warning. This is an advanced method. You should probably be using [Connection.createRemoteObject]
*
*
*
*
* Returns a proxy object that implements the specified interface, and the methods invoked on the proxy object will be invoked
* remotely.
*
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the [ ][RemoteObject.setResponseTimeout].
*
*
* If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must not be
* called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a void return value can be
* called on the update thread.
*
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side will
* have the proxy object replaced with the registered object.
*
* @see RemoteObject
*
* @param rmiId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID
* @param iFace this is the RMI interface
*/
fun getProxyObject(connection: Connection_, rmiId: Int, iFace: Class<*>?): RemoteObject {
requireNotNull(iFace) { "iface cannot be null." }
require(iFace.isInterface) { "iface must be an interface." }
// we want to have a connection specific cache of IDs
// because this is PER CONNECTION, there is no need for synchronize(), since there will not be any issues with concurrent
// access, but there WILL be issues with thread visibility because a different worker thread can be called for different connections
var remoteObject = proxyIdCache[rmiId]
if (remoteObject == null) {
// duplicates are fine, as they represent the same object (as specified by the ID) on the remote side.
// the ACTUAL proxy is created in the connection impl. Our proxy handler MUST BE suspending because of:
// 1) how we send data on the wire
// 2) how we must (sometimes) wait for a response
val proxyObject = RmiClient(connection, this, rmiId, iFace)
proxyListeners.add(proxyObject.listener)
// This is the interface inheritance by the proxy object
val interfaces: Array<Class<*>> = arrayOf(RemoteObject::class.java, iFace)
remoteObject = Proxy.newProxyInstance(RmiServer::class.java.classLoader, interfaces, proxyObject) as RemoteObject
proxyIdCache[rmiId] = remoteObject
}
return remoteObject
}
}

View File

@ -1,259 +0,0 @@
/*
* Copyright 2019 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.rmi
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.Configuration
import dorkbox.network.connection.*
import dorkbox.network.serialization.NetworkSerializationManager
import dorkbox.network.store.NullSettingsStore
import io.netty.buffer.ByteBuf
import org.slf4j.LoggerFactory
import java.io.File
import javax.crypto.SecretKey
class NopRmiConnection : Connection_ {
val logger = LoggerFactory.getLogger("RMI_NO_OP")
val rmiBridgeUnnecessaryMaybe = RmiServer(logger, true)
val config = Configuration().apply {
aeronLogDirectory = File("")
settingsStore = NullSettingsStore()
serialization = object : NetworkSerializationManager {
override fun <Iface, Impl : Iface> registerRmi(ifaceClass: Class<Iface>, implClass: Class<Impl>): NetworkSerializationManager {
return this
}
override fun takeKryo(): KryoExtra {
TODO("Not yet implemented")
}
// override fun readWithCompression(connection: Connection_, length: Int): Any {
// return false
// }
override fun <T> getRmiImpl(iFace: Class<T>): Class<T> {
TODO("Not yet implemented")
}
override fun readFullClassAndObject(input: Input?): Any {
return false
}
// override fun write(connection: Connection_, message: Any) {
// }
override fun write(buffer: ByteBuf, message: Any) {
}
override fun initialized(): Boolean {
return false
}
override fun getMethods(classID: Int): Array<CachedMethod> {
TODO("Not yet implemented")
}
override fun finishInit(endPointClass: Class<*>) {
}
// override fun writeWithCompression(connection: Connection_, message: Any) {
// }
override fun getKryoRegistrationDetails(): ByteArray {
return ByteArray(0)
}
override fun writeFullClassAndObject(output: Output?, value: Any?) {
}
override fun <T> register(clazz: Class<T>): NetworkSerializationManager {
return this
}
override fun <T> register(clazz: Class<T>, id: Int): NetworkSerializationManager {
return this
}
override fun <T> register(clazz: Class<T>, serializer: Serializer<T>): NetworkSerializationManager {
return this
}
override fun <T> register(clazz: Class<T>, serializer: Serializer<T>, id: Int): NetworkSerializationManager {
return this
}
override fun verifyKryoRegistration(bytes: ByteArray): Boolean {
return false
}
// override fun writeWithCrypto(connection: Connection_, message: Any) {
// }
override fun returnKryo(kryo: KryoExtra) {
}
// override fun read(connection: Connection_, length: Int): Any {
// return false
// }
override fun read(buffer: ByteBuf, length: Int): Any {
return false
}
// override fun readWithCrypto(connection: Connection_, length: Int): Any {
// return false
// }
}
}
val connectionManager2: ConnectionManager<Connection_> = ConnectionManager(logger, config)
val rmiEndpoint: EndPoint<Connection_> = object : EndPoint<Connection_>(Any::class.java, config) {
override val connectionManager: ConnectionManager<Connection_> = connectionManager2
override fun isConnected(): Boolean { return false }
}
val con = object : Connection_ {
override fun pollSubscriptions(): Int {
return 0
}
override fun rmiSupport(): ConnectionRmiSupport {
TODO("Not yet implemented")
}
override fun nextGcmSequence(): Long {
TODO("Not yet implemented")
}
override fun cryptoKey(): SecretKey {
TODO("Not yet implemented")
}
override fun isExpired(now: Long): Boolean {
return false
}
override fun isClosed(): Boolean {
return true
}
override fun endPoint(): EndPoint<*> {
TODO("Not yet implemented")
}
override fun hasRemoteKeyChanged(): Boolean {
TODO("Not yet implemented")
}
override val subscriptionPort = 0
override val publicationPort = 0
override val remoteAddress = "0.0.0.0"
override val remoteAddressInt = 0
override val streamId = 0
override val sessionId = 0
override val isLoopback = false
override val isIPC = false
override val isNetwork = false
override suspend fun send(message: Any) {
TODO("Not yet implemented")
}
override suspend fun send(message: Any, priority: Byte) {
TODO("Not yet implemented")
}
override suspend fun ping(): Ping {
TODO("Not yet implemented")
}
override fun close() {
TODO("Not yet implemented")
}
override suspend fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>) {
TODO("Not yet implemented")
}
override suspend fun <Iface> getRemoteObject(objectId: Int, callback: RemoteObjectCallback<Iface>) {
TODO("Not yet implemented")
}
}
val rmiSUpport = ConnectionRmiSupport(rmiBridgeUnnecessaryMaybe)
init {
println("CREATED RMI NO OP")
}
override fun hasRemoteKeyChanged(): Boolean {
return false
}
override val subscriptionPort = 0
override val publicationPort = 0
override val remoteAddress = ""
override val remoteAddressInt = 0
override val isLoopback = false
override val isIPC = false
override val isNetwork = false
override fun endPoint(): EndPoint<*> {
return rmiEndpoint
}
override suspend fun send(message: Any) {}
override suspend fun send(message: Any, priority: Byte) {}
override suspend fun ping(): Ping {
TODO("Not yet implemented")
}
override fun close() {}
override suspend fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>) {}
override suspend fun <Iface> getRemoteObject(objectId: Int, callback: RemoteObjectCallback<Iface>) {}
override fun nextGcmSequence(): Long {
return 0
}
override fun cryptoKey(): SecretKey {
TODO("Not yet implemented")
}
override fun pollSubscriptions(): Int {
return 0
}
override fun isExpired(now: Long): Boolean {
return false
}
override val streamId: Int = 0
override val sessionId: Int = 0
override fun rmiSupport(): ConnectionRmiSupport {
return rmiSUpport
}
override fun isClosed(): Boolean {
return false
}
}

View File

@ -47,19 +47,23 @@ interface RemoteObject {
var responseTimeout: Int
/**
* Sets the blocking behavior when invoking a remote method. Default is false (blocking).
*
* @param enable If false, the invoking thread will wait for the remote method to return or timeout.
* If true, the invoking thread will not wait for a response. The method will return immediately and the return value
* should be ignored.
*
* WHEN TRUE, it will be impossible to
*
* If return values are being transmitted, the return value or any thrown exception can later be retrieved with
* [.waitForLastResponse] or [.waitForResponse(id)]. The responses will be stored until retrieved, so each method call
* should have a matching retrieve.
* @return the ID of response for the last method invocation.
*/
fun setAsync(enable: Boolean)
val lastResponseId: Int
/**
* Sets the behavior when invoking a remote method. Default is false.
*
* If true, the invoking thread will not wait for a response. The method will return immediately and the return value
* should be ignored.
*
* If false, the invoking thread will wait (if called via suspend, then it will use coroutines) for the remote method to return or
* timeout.
*
* The return value or any thrown exception can later be retrieved with [RemoteObject.waitForLastResponse] or [RemoteObject.waitForResponse].
* The responses will be stored until retrieved, so each method call should have a matching retrieve.
*/
var async: Boolean
/**
* Permits calls to [Object.toString] to actually return the `toString()` method on the object.
@ -69,26 +73,37 @@ interface RemoteObject {
*/
fun enableToString(enableDetailedToString: Boolean)
/**
* Permits calls to [RemoteObject.waitForLastResponse] and [RemoteObject.waitForResponse] to actually wait for a response.
*
* You must be in ASYNC mode already for this to work. There will be undefined errors if you do not enable waiting
* BEFORE calling the method you want to wait for
*
* @param enableWaiting if true, you want wait for the method results. If false, undefined errors can happen while waiting
*/
fun enableWaitingForResponse(enableWaiting: Boolean)
/**
* Waits for the response to the last method invocation to be received or the response timeout to be reached.
*
* You must be in ASYNC mode + enabled waiting for this to work. There will be undefined errors if you do not enable waiting BEFORE
* calling the method you want to wait for
*
* @return the response of the last method invocation
*/
fun waitForLastResponse(): Any?
/**
* @return the ID of response for the last method invocation.
*/
val lastResponseID: Byte
suspend fun waitForLastResponse(): Any?
/**
* Waits for the specified method invocation response to be received or the response timeout to be reached.
*
* @param responseID this is the response ID obtained via [.getLastResponseID]
* You must be in ASYNC mode + enabled waiting for this to work. There will be undefined errors if you do not enable waiting BEFORE
* calling the method you want to wait for
*
* @param responseId usually this is the response ID obtained via [RemoteObject.lastResponseId]
*
* @return the response of the last method invocation
*/
fun waitForResponse(responseID: Byte): Any?
suspend fun waitForResponse(responseId: Int): Any?
/**
* Causes this RemoteObject to stop listening to the connection for method invocation response messages.

View File

@ -18,9 +18,10 @@ package dorkbox.network.rmi
/**
* Callback for creating remote RMI classes
*/
@FunctionalInterface
interface RemoteObjectCallback<Iface> {
/**
* @param remoteObject the remote object (as a proxy object) or null if there was an error creating the RMI object
*/
fun created(remoteObject: Iface)
suspend fun created(remoteObject: Iface)
}

View File

@ -0,0 +1,286 @@
/*
* Copyright 2020 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.rmi
import dorkbox.util.collections.LockFreeIntBiMap
import mu.KLogger
import org.agrona.collections.IntArrayList
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
/**
* This class allows you to store objects in it via an ID.
*
*
* The ID can be reserved ahead of time, or it can be dynamically generated. Additionally, this class will recycle IDs, and prevent
* reserved IDs from being dynamically selected.
*
* ADDITIONALLY, these IDs are limited to SHORT size (65535 max value) because when executing remote methods, a lot, it is important to
* have as little data overhead in the message as possible.
*
* These data structures are not SHORTs because the JVM doesn't have good support for SHORT.
*
* From https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf
*
* The Java Virtual Machine provides the most direct support for data of type int. This is partly in anticipation of efficient
* implementations of the Java Virtual Machine's operand stacks and local variable arrays. It is also motivated by the frequency of
* int data in typical programs. Other integral types have less direct support. There are no byte, char, or short versions of the
* store, load, or add instructions, for instance.
*
*
* In situations where we want to pass in the Connection (to an RMI method) as a parameter, we have to be able to override method A,
* with method B.
*
* This is to support calling RMI methods from an interface (that does pass the connection reference) to an implType, that DOES pass
* the connection reference. The remote side (that initiates the RMI calls), MUST use the interface, and the implType may override
* the method, so that we add the connection as the first in the list of parameters.
*
*
* for example:
* Interface: foo(String x)
* Impl: foo(Connection c, String x)
*
*
* The implType (if it exists, with the same name, and with the same signature + connection parameter) will be called from the interface
* instead of the method that would NORMALLY be called.
*
* @author Nathan Robinson
*/
class RemoteObjectStorage(val logger: KLogger) {
companion object {
const val INVALID_RMI = 0
}
// this is the ID -> Object RMI map. The RMI ID is used (not the kryo ID)
private val objectMap = LockFreeIntBiMap<Any>(INVALID_RMI)
private val idLock = ReentrantReadWriteLock()
// object ID's are assigned OR requested, so we construct the data structures differently
// there are 2 ways to get an RMI object ID
// 1) request the next number from the counter
// 2) specifically request a number
// To solve this, we use 3 data structures, because it's also possible to RETURN no-longer needed object ID's (like when a
// connection closes)
private var objectIdCounter: Int = 1
private val reservedObjectIds = IntArrayList(1, INVALID_RMI)
private val objectIds = IntArrayList(16, INVALID_RMI)
init {
(0..8).forEach { _ ->
objectIds.addInt(objectIdCounter++)
}
}
private fun validate(objectId: Int) {
require(objectId > 0) { "The ID must be greater than 0" }
require(objectId <= 65535) { "The ID must be less than 65,535" }
}
/**
* @return the next ID or 0 (INVALID_RMI, if it's invalid)
*/
private fun unsafeNextId(): Int {
val id = if (objectIds.size > 0) {
objectIds.removeAt(objectIds.size - 1)
} else {
objectIdCounter++
}
if (objectIdCounter > 65535) {
// basically, it's a short (but collections are a LOT easier to deal with if it's an int)
val msg = "Max ID size is 65535, because of how we pack the bytes when sending RMI messages. FATAL ERROR! (too many objects)"
logger.error(msg)
return INVALID_RMI
}
return id
}
/**
* @return the next possible RMI object ID. Either one that is next available, or 0 (INVALID_RMI) if it was invalid
*/
fun nextId(): Int {
idLock.write {
var idToReturn = unsafeNextId()
while (reservedObjectIds.contains(idToReturn)) {
idToReturn = unsafeNextId()
}
return idToReturn
}
}
/**
* Reserves an ID so that other requests for ID's will never return this ID. The number must be > 0 and < 65535
*
* Reservations are permanent and it will ALWAYS be reserved! You cannot "un-reserve" an ID.
*
* If you care about memory and performance, use the ID from "nextId()" instead.
*
* @return false if this ID was not able to be reserved
*/
fun reserveId(id: Int): Boolean {
validate(id)
idLock.write {
val contains = objectIds.remove(id)
if (contains) {
// this id is available for us to use (and was temporarily used before)
return true
}
if (reservedObjectIds.contains(id)) {
// this id is ALREADY used by something else
return false
}
if (objectIdCounter < id) {
// this id is ALREADY used by something else
return false
}
if (objectIdCounter == id) {
// we are available via the counter, so make sure the counter increments
objectIdCounter++
// we still want to mark this as reserved, so fall through
}
// this means that the counter is LARGER than the id (maybe even a LOT larger)
// we just stuff this requested number in a small array and check it whenever we get a new number
reservedObjectIds.add(id)
return true
}
}
/**
* @return an ID to be used again. Reserved IDs will not be allowed to be returned
*/
fun returnId(id: Int) {
idLock.write {
if (reservedObjectIds.contains(id)) {
logger.error {
"Do not return a reserved ID ($id). Once an ID is reserved, it is permanent."
}
return
}
val shortCheck: Int = (id + 1)
if (shortCheck == objectIdCounter) {
objectIdCounter--
} else {
objectIds.add(id)
}
return
}
}
/**
* Automatically registers an object with the next available ID to allow a remote connection to access this object via the returned ID
*
* @return the RMI object ID, there are too many, it will fail with a Runtime Exception. (Max limit is 65535 objects)
*/
fun register(`object`: Any): Int {
// this will return INVALID_RMI if there are too many in the ObjectSpace
val nextObjectId = nextId()
if (nextObjectId != INVALID_RMI) {
objectMap.put(nextObjectId, `object`)
logger.trace {
"Object <proxy #$nextObjectId> registered with .toString() = '${`object`}'"
}
}
return nextObjectId
}
/**
* Registers an object to allow a remote connection access to this object via the specified ID
*
* @param objectId Must not be <= 0 or > 65535
* @return true if successful, false if there was an error
*/
fun register(objectId: Int, `object`: Any): Boolean {
validate(objectId)
objectMap.put(objectId, `object`)
logger.trace {
"Object <proxy #${objectId}> registered with .toString() = '${`object`}'"
}
return true
}
/**
* Removes an object. The remote connection will no longer be able to access it.
*/
fun <T> remove(objectId: Int): T {
validate(objectId)
val rmiObject = objectMap.remove(objectId) as T
returnId(objectId)
logger.trace {
"Object <proxy #${objectId}> removed with .toString() = '${rmiObject}'"
}
@Suppress("UNCHECKED_CAST")
return rmiObject
}
/**
* Removes an object, and the remote end of the RmiBridge connection will no longer be able to access it.
*/
fun remove(remoteObject: Any) {
val objectId = objectMap.inverse().remove(remoteObject)
if (objectId == INVALID_RMI) {
logger.error("Object {} could not be found in the ObjectSpace.", remoteObject)
} else {
returnId(objectId)
logger.trace {
"Object '${remoteObject}' (ID: ${objectId}) removed from RMI system."
}
}
}
/**
* @return the object registered with the specified ID.
*/
operator fun get(objectId: Int): Any {
validate(objectId)
return objectMap[objectId]
}
/**
* @return the ID registered for the specified object, or INVALID_RMI if not found.
*/
fun <T> getId(remoteObject: T): Int {
// Find an ID with the object.
return objectMap.inverse()[remoteObject]
}
fun close() {
objectMap.clear()
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2010 dorkbox, llc
* Copyright 2020 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -12,64 +12,37 @@
* 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.
*
* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package dorkbox.network.rmi
import com.conversantmedia.util.concurrent.MultithreadConcurrentQueue
import dorkbox.network.connection.Connection
import dorkbox.network.connection.Connection_
import dorkbox.network.connection.KryoExtra
import dorkbox.network.connection.OnMessageReceived
import dorkbox.network.other.SuspendWaiter
import dorkbox.network.other.invokeSuspendFunction
import dorkbox.network.rmi.messages.MethodRequest
import dorkbox.network.rmi.messages.MethodResponse
import kotlinx.coroutines.launch
import org.agrona.collections.IntArrayList
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException
import kotlinx.coroutines.runBlocking
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import java.util.*
import kotlin.coroutines.Continuation
/**
* Handles network communication when methods are invoked on a proxy.
* Handles network communication when methods are invoked on a proxy. For NON-BLOCKING performance, the interface
* must have the 'suspend' keyword added. If it is not present, then all method invocation will be BLOCKING.
*
*
* If the method return type is 'void', then we don't have to explicitly set 'transmitReturnValue' to false
*
*
* If there are no checked exceptions thrown, then we don't have to explicitly set 'transmitExceptions' to false
*
* @param connection this is really the network client -- there is ONLY ever 1 connection
* @param rmiSupport is used to provide RMI support
* @param rmiId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID
* @param iFace this is the RMI interface
* @param rmiObjectId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID
* @param cachedMethods this is the methods available for the specified class
*/
class RmiClient(private val connection: Connection_,
private val rmiSupport: ConnectionRmiSupport, // this is the RMI id
class RmiClient(val isGlobal: Boolean,
val rmiObjectId: Int,
iFace: Class<*>?) : InvocationHandler {
private val connection: Connection,
private val proxyString: String,
private val rmiSupportCache: RmiSupportCache,
private val cachedMethods: Array<CachedMethod>) : InvocationHandler {
companion object {
private val methods = RmiUtils.getMethods(RemoteObject::class.java)
@ -78,156 +51,31 @@ class RmiClient(private val connection: Connection_,
private val setResponseTimeoutMethod = methods.find { it.name == "setResponseTimeout" }
private val getResponseTimeoutMethod = methods.find { it.name == "getResponseTimeout" }
private val setAsyncMethod = methods.find { it.name == "setAsync" }
private val getAsyncMethod = methods.find { it.name == "getAsync" }
private val enableToStringMethod = methods.find { it.name == "enableToString" }
private val enableWaitingForResponseMethod = methods.find { it.name == "enableWaitingForResponse" }
private val waitForLastResponseMethod = methods.find { it.name == "waitForLastResponse" }
private val getLastResponseIDMethod = methods.find { it.name == "getLastResponseID" }
private val getLastResponseIdMethod = methods.find { it.name == "getLastResponseId" }
private val waitForResponseMethod = methods.find { it.name == "waitForResponse" }
private val toStringMethod = methods.find { it.name == "toString" }
@Suppress("UNCHECKED_CAST")
private val EMPTY_ARRAY: Array<Any> = Collections.EMPTY_LIST.toTypedArray() as Array<Any>
}
private val logger: Logger
private val responseWaiter = SuspendWaiter()
private val lock = ReentrantLock()
private val responseCondition = lock.newCondition()
private val responseTable = arrayOfNulls<MethodResponse>(256)
private val pendingResponses = BooleanArray(256)
// this is the KRYO class id
private val classId: Int
private val proxyString = "<proxy #$rmiObjectId>"
val listener: OnMessageReceived<Connection, MethodResponse>
private var timeoutMillis = 3000
private var timeoutMillis: Long = 3000
private var isAsync = false
private var allowWaiting = false
private var enableToString = false
// for responseId's, "0" means no response (or invalid response id)
private val responseIds = MultithreadConcurrentQueue<Byte>(256)
private var previousResponseId: Byte = 0x00
// this is really a a short!
@Volatile
private var previousResponseId: Int = 0
init {
// create a shuffled list of ID's
val ids = IntArrayList()
for (id in 1..255) {
ids.addInt(id)
}
ids.shuffle()
// populate the array of randomly assigned ID's.
ids.forEach {
responseIds.offer(it.toByte())
}
// figure out the class ID for this RMI object
val endPoint = connection.endPoint()
val serializationManager = endPoint.serialization
var kryoExtra: KryoExtra? = null
try {
kryoExtra = serializationManager.takeKryo()
classId = kryoExtra.getRegistration(iFace).id
} finally {
if (kryoExtra != null) {
serializationManager.returnKryo(kryoExtra)
}
}
logger = LoggerFactory.getLogger(endPoint.name + ":" + this.javaClass.simpleName)
// this listener is called when the "server" responds to our request.
// SPECIFICALLY, this is "unblocks" our "waiting for response" logic
listener = object : OnMessageReceived<Connection, MethodResponse> {
override fun received(connection: Connection, message: MethodResponse) {
// we have to check object ID's, BECAUSE we can have different objects all listening for a response
if (message.objectId != rmiObjectId) {
return
}
val responseIdAsInt = 0xFF and message.responseId.toInt()
synchronized(this) {
if (pendingResponses[responseIdAsInt]) {
responseTable[responseIdAsInt] = message
}
}
lock.lock()
try {
responseCondition.signalAll()
} finally {
lock.unlock()
}
}
}
}
@Throws(Exception::class)
override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
val declaringClass = method.declaringClass
if (declaringClass == RemoteObject::class.java) {
// manage all of the RemoteObject proxy methods
when (method) {
closeMethod -> {
rmiSupport.removeProxyObject(this)
return null
}
setResponseTimeoutMethod -> {
timeoutMillis = args!![0] as Int
return null
}
getResponseTimeoutMethod -> {
return timeoutMillis
}
setAsyncMethod -> {
isAsync = args!![0] as Boolean
return null
}
enableToStringMethod -> {
enableToString = args!![0] as Boolean
return null
}
waitForLastResponseMethod -> {
return waitForResponse(0xFF and previousResponseId.toInt())
}
getLastResponseIDMethod -> {
return previousResponseId
}
waitForResponseMethod -> {
check(!isAsync) { "This RemoteObject is not currently set to ASYNC mode. Unable to ignore all responses." }
return waitForResponse(args!![0] as Int)
}
else -> throw Exception("Invocation handler could not find RemoteObject method for ${method.name}")
}
} else if (!enableToString && method == toStringMethod) {
return proxyString
}
val invokeMethod = MethodRequest()
invokeMethod.objectId = rmiObjectId
invokeMethod.args = args
// which method do we access? We always want to access the IMPLEMENTATION (if available!)
val cachedMethods = connection.endPoint().serialization.getMethods(classId)
var i = 0
val n = cachedMethods.size
while (i < n) {
val cachedMethod = cachedMethods[i]
val checkMethod = cachedMethod.method
if (checkMethod == method) {
invokeMethod.cachedMethod = cachedMethod
break
}
i++
}
// a value of 0 means there is no response (for 'async' calls only)
val responseId: Byte
val responseIdAsInt: Int
val returnType = method.returnType
private suspend fun invokeSuspend(method: Method, args: Array<Any>): Any? {
// there is a STRANGE problem, where if we DO NOT respond/reply to method invocation, and immediate invoke multiple methods --
// the "server" side can have out-of-order method invocation. There are 2 ways to solve this
// 1) make the "server" side single threaded
@ -237,86 +85,31 @@ class RmiClient(private val connection: Connection_,
// response (even if it is a void response). This simplifies our response mask, and lets us use more bits for storing the
// response ID
val responseStorage = rmiSupportCache.getResponseStorage()
// If we are async, we always ignore the response.
// A value of 0 means to not respond, and the rest is just an ID to match requests <-> responses
if (isAsync) {
responseId = 0x00
responseIdAsInt = 0
} else {
responseId = responseIds.poll()
responseIdAsInt = 0xFF and responseId.toInt()
// If we are async, we ignore the response.... FOR NOW. The response, even if there is NOT one (ie: not void) will always return
// a thing (so we will know when to stop blocking).
val responseId = responseStorage.prep(rmiObjectId, responseWaiter)
synchronized(this) {
pendingResponses[responseIdAsInt] = true
}
invokeMethod.responseId = responseId
}
// so we can query this, if necessary
// so we can query for async, if we want to necessary
previousResponseId = responseId
val invokeMethod = MethodRequest()
invokeMethod.isGlobal = isGlobal
invokeMethod.objectId = rmiObjectId
invokeMethod.responseId = responseId
invokeMethod.args = args
// Sends our invokeMethod to the remote connection, which the RmiBridge listens for
val endPoint = connection.endPoint()
endPoint.actionDispatch.launch {
connection.send(invokeMethod);
if (logger.isTraceEnabled) {
var argString = args?.contentDeepToString() ?: ""
argString = argString.substring(1, argString.length - 1)
logger.trace("$connection sent: ${method.declaringClass.simpleName}#${method.name}($argString)")
}
}
// // if a 'suspend' function is called, then our last argument is a 'Continuation' object (and we can use that instead of runBlocking)
// val continuation = args?.lastOrNull()
// if (continuation is Continuation<*>) {
//// val continuation = args!!.last() as Continuation<*>
//// val argsWithoutContinuation = args.take(args.size - 1)
//// continuation.context.
// return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
// }
// MUST use 'waitForLastResponse()' or 'waitForResponse'('getLastResponseID()') to get the response
// If we are async then we return immediately
// If we are 'void' return type and do not throw checked exceptions then we return immediately
if (isAsync) {
if (returnType.isPrimitive) {
if (returnType == Int::class.javaPrimitiveType) {
return 0
}
if (returnType == Boolean::class.javaPrimitiveType) {
return java.lang.Boolean.FALSE
}
if (returnType == Float::class.javaPrimitiveType) {
return 0.0f
}
if (returnType == Char::class.javaPrimitiveType) {
return 0.toChar()
}
if (returnType == Long::class.javaPrimitiveType) {
return 0L
}
if (returnType == Short::class.javaPrimitiveType) {
return 0.toShort()
}
if (returnType == Byte::class.javaPrimitiveType) {
return 0.toByte()
}
if (returnType == Double::class.javaPrimitiveType) {
return 0.0
}
}
return null
}
// which method do we access? We always want to access the IMPLEMENTATION (if available!). we know that this will always succeed
// this should be accessed via the KRYO class ID + method index (both are SHORT, and can be packed)
invokeMethod.cachedMethod = cachedMethods.first { it.method == method }
connection.send(invokeMethod)
// if we are async, then this will immediately return!
return try {
val result = waitForResponse(responseIdAsInt)
val result = responseStorage.waitForReply(allowWaiting, isAsync, rmiObjectId, responseId, responseWaiter, timeoutMillis)
if (result is Exception) {
throw result
} else {
@ -324,58 +117,146 @@ class RmiClient(private val connection: Connection_,
}
} catch (ex: TimeoutException) {
throw TimeoutException("Response timed out: ${method.declaringClass.name}.${method.name}")
} finally {
synchronized(this) {
pendingResponses[responseIdAsInt] = false
responseTable[responseIdAsInt] = null
}
}
}
/**
* A timeout of 0 means that we want to disable waiting, otherwise - it waits in milliseconds
*/
@Throws(IOException::class)
private fun waitForResponse(responseIdAsInt: Int): Any? {
// if timeout == 0, we wait "forever"
var remaining: Long
val endTime: Long
@Suppress("DuplicatedCode")
@Throws(Exception::class)
override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
if (method.declaringClass == RemoteObject::class.java) {
// manage all of the RemoteObject proxy methods
when (method) {
closeMethod -> {
rmiSupportCache.removeProxyObject(rmiObjectId)
return null
}
setResponseTimeoutMethod -> {
timeoutMillis = (args!![0] as Int).toLong()
return null
}
getResponseTimeoutMethod -> {
return timeoutMillis.toInt()
}
getAsyncMethod -> {
return isAsync
}
setAsyncMethod -> {
isAsync = args!![0] as Boolean
return null
}
enableToStringMethod -> {
enableToString = args!![0] as Boolean
return null
}
getLastResponseIdMethod -> {
// only ASYNC can wait for responses
check(isAsync) { "This RemoteObject is not currently set to ASYNC mode. Unable to manually get the response ID." }
check(allowWaiting) { "This RemoteObject does not allow waiting for responses. You must enable this BEFORE " +
"calling the method that you want to wait for the respose to" }
if (timeoutMillis != 0) {
remaining = timeoutMillis.toLong()
endTime = System.currentTimeMillis() + remaining
} else {
// not forever, but close enough
remaining = Long.MAX_VALUE
endTime = Long.MAX_VALUE
return previousResponseId
}
enableWaitingForResponseMethod -> {
allowWaiting = args!![0] as Boolean
return null
}
waitForLastResponseMethod -> {
// only ASYNC can wait for responses
check(isAsync) { "This RemoteObject is not currently set to ASYNC mode. Unable to manually wait for a response." }
check(allowWaiting) { "This RemoteObject does not allow waiting for responses. You must enable this BEFORE " +
"calling the method that you want to wait for the respose to" }
val maybeContinuation = args?.lastOrNull() as Continuation<*>
// this is a suspend method, so we don't need extra checks
return invokeSuspendFunction(maybeContinuation) {
rmiSupportCache.getResponseStorage().waitForReplyManually(rmiObjectId, previousResponseId, responseWaiter)
}
}
waitForResponseMethod -> {
// only ASYNC can wait for responses
check(isAsync) { "This RemoteObject is not currently set to ASYNC mode. Unable to manually wait for a response." }
check(allowWaiting) { "This RemoteObject does not allow waiting for responses. You must enable this BEFORE " +
"calling the method that you want to wait for the respose to" }
val maybeContinuation = args?.lastOrNull() as Continuation<*>
// this is a suspend method, so we don't need extra checks
return invokeSuspendFunction(maybeContinuation) {
rmiSupportCache.getResponseStorage().waitForReplyManually(rmiObjectId, args[0] as Int, responseWaiter)
}
}
else -> throw Exception("Invocation handler could not find RemoteObject method for ${method.name}")
}
} else if (!enableToString && method == toStringMethod) {
return proxyString
}
// wait for the specified time
var methodResponse: MethodResponse?
while (remaining > 0) {
synchronized(this) {
methodResponse = responseTable[responseIdAsInt]
}
// if a 'suspend' function is called, then our last argument is a 'Continuation' object
// We will use this for our coroutine context instead of running on a new coroutine
val maybeContinuation = args?.lastOrNull()
if (methodResponse != null) {
previousResponseId = 0 // 0 is "no response" or "invalid"
return methodResponse!!.result
if (isAsync) {
// return immediately, without suspends
if (maybeContinuation is Continuation<*>) {
val argsWithoutContinuation = args.take(args.size - 1)
invokeSuspendFunction(maybeContinuation) {
invokeSuspend(method, argsWithoutContinuation.toTypedArray())
}
} else {
lock.lock()
try {
responseCondition.await(remaining, TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
Thread.currentThread()
.interrupt()
throw IOException("Response timed out.", e)
} finally {
lock.unlock()
runBlocking {
invokeSuspend(method, args ?: EMPTY_ARRAY)
}
}
// if we are async then we return immediately. If you want the response value, you MUST use
// 'waitForLastResponse()' or 'waitForResponse'('getLastResponseID()')
val returnType = method.returnType
if (returnType.isPrimitive) {
return when (returnType) {
Int::class.javaPrimitiveType -> {
0
}
Boolean::class.javaPrimitiveType -> {
java.lang.Boolean.FALSE
}
Float::class.javaPrimitiveType -> {
0.0f
}
Char::class.javaPrimitiveType -> {
0.toChar()
}
Long::class.javaPrimitiveType -> {
0L
}
Short::class.javaPrimitiveType -> {
0.toShort()
}
Byte::class.javaPrimitiveType -> {
0.toByte()
}
Double::class.javaPrimitiveType -> {
0.0
}
else -> {
null
}
}
}
return null
} else {
// non-async code, so we will be blocking/suspending!
return if (maybeContinuation is Continuation<*>) {
val argsWithoutContinuation = args.take(args.size - 1)
invokeSuspendFunction(maybeContinuation) {
invokeSuspend(method, argsWithoutContinuation.toTypedArray())
}
} else {
runBlocking {
invokeSuspend(method, args ?: EMPTY_ARRAY)
}
}
remaining = endTime - System.currentTimeMillis()
}
throw TimeoutException("Response timed out.")
}
override fun hashCode(): Int {
@ -395,7 +276,11 @@ class RmiClient(private val connection: Connection_,
if (javaClass != other.javaClass) {
return false
}
val other1 = other as RmiClient
return rmiObjectId == other1.rmiObjectId
if (other !is RmiClient) {
return false
}
return rmiObjectId == other.rmiObjectId
}
}

View File

@ -0,0 +1,145 @@
package dorkbox.network.rmi
import com.conversantmedia.util.concurrent.MultithreadConcurrentQueue
import dorkbox.network.other.SuspendWaiter
import dorkbox.network.rmi.messages.MethodResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.agrona.collections.Hashing
import org.agrona.collections.Int2NullableObjectHashMap
import org.agrona.collections.IntArrayList
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
/**
* Manages the "pending response" from method invocation.
*
* response ID's and the memory they hold will leak if the response never arrives!
*/
class RmiResponseStorage(private val actionDispatch: CoroutineScope) {
// Response ID's are for ALL in-flight RMI on the network stack. instead of limited to (originally) 64, we are now limited to 65,535
// these are just looped around in a ring buffer.
// These are stored here as int, however these are REALLY shorts and are int-packed when transferring data on the wire
private val rmiResponseIds = MultithreadConcurrentQueue<Int>(65535)
private val pendingLock = ReentrantReadWriteLock()
private val pending = Int2NullableObjectHashMap<Any>(32, Hashing.DEFAULT_LOAD_FACTOR, true)
init {
// create a shuffled list of ID's. This operation is ONLY performed ONE TIME per endpoint!
val ids = IntArrayList()
for (id in Short.MIN_VALUE..Short.MAX_VALUE) {
ids.addInt(id)
}
ids.shuffle()
// populate the array of randomly assigned ID's.
ids.forEach {
rmiResponseIds.offer(it)
}
}
// resume any pending remote object method invocations (if they are not async, or not manually waiting)
suspend fun onMessage(message: MethodResponse) {
val objectId = message.objectId
val responseId = message.responseId
val result = message.result
val pendingId = RmiUtils.packShorts(objectId, responseId)
val previous = pendingLock.write { pending.put(pendingId, result) }
// if NULL, since either we don't exist, or it was cancelled
if (previous is SuspendWaiter) {
// this means we were NOT timed out!
previous.doNotify()
}
// always return the responseId! It will (hopefully) be a while before this ID is used again
rmiResponseIds.offer(responseId)
}
fun prep(rmiObjectId: Int, responseWaiter: SuspendWaiter): Int {
val responseId = rmiResponseIds.poll()
// we pack them together so we can fully use the range of ints, so we can service ALL rmi requests in a single spot
pendingLock.write { pending[RmiUtils.packShorts(rmiObjectId, responseId)] = responseWaiter }
return responseId
}
suspend fun waitForReply(allowWaiting: Boolean, isAsync: Boolean, rmiObjectId: Int, responseId: Int,
responseWaiter: SuspendWaiter, timeoutMillis: Long): Any? {
val pendingId = RmiUtils.packShorts(rmiObjectId, responseId)
var delayJobForTimeout: Job? = null
if (!(isAsync && allowWaiting) && timeoutMillis > 0L) {
// always launch a "cancel" coroutine, unless we want to wait forever
delayJobForTimeout = actionDispatch.launch {
delay(timeoutMillis)
val previous = pendingLock.write {
val prev = pending.remove(pendingId)
if (prev is SuspendWaiter) {
pending[pendingId] = TimeoutException("Response timed out.")
}
prev
}
// if we are NOT SuspendWaiter, then it means we had a result!
//
// If there are tight timing issues, then we err on the side of "you timed out"
if (!isAsync) {
// we only cancel waiting because when NON-ASYNC
if (previous is SuspendWaiter) {
previous.cancel()
}
}
}
}
return if (isAsync) {
null
} else {
waitForReplyManually(pendingId, responseWaiter, delayJobForTimeout)
}
}
// this is called when we MANUALLY want to wait for a reply as part of async!
// A timeout of 0 means we wait forever
suspend fun waitForReplyManually(rmiObjectId: Int, responseId: Int, responseWaiter: SuspendWaiter): Any? {
val pendingId = RmiUtils.packShorts(rmiObjectId, responseId)
return waitForReplyManually(pendingId, responseWaiter, null)
}
// we have to be careful when we resume, because SOMEONE ELSE'S METHOD RESPONSE can resume us (but only from the same object)!
private suspend fun waitForReplyManually(pendingId: Int,responseWaiter: SuspendWaiter, delayJobForTimeout: Job?): Any? {
while(true) {
val checkResult = pendingLock.read { pending[pendingId] }
if (checkResult !is SuspendWaiter) {
// this means we have correct data! (or it was an exception) we can safely remove the data from the map
pendingLock.write { pending.remove(pendingId) }
delayJobForTimeout?.cancel()
return checkResult
}
// keep waiting, since we don't have a response yet
responseWaiter.doWait()
}
}
fun close() {
rmiResponseIds.clear()
pendingLock.write { pending.clear() }
}
}

View File

@ -1,267 +0,0 @@
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package dorkbox.network.rmi
import dorkbox.network.connection.Connection
import dorkbox.network.rmi.messages.MethodRequest
import dorkbox.network.rmi.messages.MethodResponse
import dorkbox.util.collections.LockFreeIntBiMap
import org.slf4j.Logger
import java.io.IOException
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
/**
* Allows methods on objects to be invoked remotely over TCP, UDP, or LOCAL. Local connections ignore TCP/UDP requests, and perform
* object transformation (because there is no serialization occurring) using a series of weak hashmaps.
*
*
* Objects are [NetworkSerializationManager.registerRmi], and endpoint connections can then [ ][Connection.createRemoteObject] for the registered objects.
*
*
* It costs at least 2 bytes more to use remote method invocation than just sending the parameters. If the method has a return value which
* is not [ignored][RemoteObject.setAsync], an extra byte is written. If the type of a parameter is not final (note that
* primitives are final) then an extra byte is written for that parameter.
*
*
* In situations where we want to pass in the Connection (to an RMI method), we have to be able to override method A, with method B.
* This is to support calling RMI methods from an interface (that does pass the connection reference) to
* an implType, that DOES pass the connection reference. The remote side (that initiates the RMI calls), MUST use
* the interface, and the implType may override the method, so that we add the connection as the first in
* the list of parameters.
*
*
* for example:
* Interface: foo(String x)
* Impl: foo(Connection c, String x)
*
*
* The implType (if it exists, with the same name, and with the same signature + connection parameter) will be called from the interface
* instead of the method that would NORMALLY be called.
*
* @author Nathan Sweet <misc></misc>@n4te.com>, Nathan Robinson
*/
class RmiServer(// the name of who created this RmiBridge
val logger: Logger, isGlobal: Boolean) {
companion object {
const val INVALID_MAP_ID = -1
const val INVALID_RMI = Int.MAX_VALUE
/**
* @return true if the objectId is global for all connections (even). false if it's connection-only (odd)
*/
fun isGlobal(objectId: Int): Boolean {
// global RMI objects -> EVEN in range 0 - (MAX_VALUE-1)
// connection local RMI -> ODD in range 1 - (MAX_VALUE-1)
return objectId and 1 == 0
}
/**
* Invokes the method on the object and, if necessary, sends the result back to the connection that made the invocation request. This
* method is invoked on the update thread of the [EndPoint] for this RmiBridge and unless an executor has been set.
*
* This is the response to the invoke method in the RmiProxyHandler
*
* @param connection
* The remote side of this connection requested the invocation.
*/
@Throws(IOException::class)
internal operator fun invoke(connection: Connection, target: Any, methodRequest: MethodRequest, logger: Logger): MethodResponse? {
val cachedMethod = methodRequest.cachedMethod
if (logger.isTraceEnabled) {
var argString = ""
if (methodRequest.args != null) {
argString = Arrays.deepToString(methodRequest.args)
argString = argString.substring(1, argString.length - 1)
}
val stringBuilder = StringBuilder(128)
stringBuilder.append(connection.toString())
.append(" received: ")
.append(target.javaClass.simpleName)
stringBuilder.append(":").append(methodRequest.objectId)
stringBuilder.append("#").append(cachedMethod.method.name)
stringBuilder.append("(").append(argString).append(")")
if (cachedMethod.overriddenMethod != null) {
// did we override our cached method? THIS IS NOT COMMON.
stringBuilder.append(" [Connection method override]")
}
logger.trace(stringBuilder.toString())
}
val responseId = methodRequest.responseId.toInt()
var result: Any?
try {
// args!! is safe to do here (even though it doesn't make sense)
result = cachedMethod.invoke(connection, target, methodRequest.args!!)
} catch (ex: Exception) {
logger.error("Error invoking method: ${cachedMethod.method.declaringClass.name}.${cachedMethod.method.name}", ex)
result = ex.cause
// added to prevent a stack overflow when references is false
// (because 'cause' == "this").
// See:
// https://groups.google.com/forum/?fromgroups=#!topic/kryo-users/6PDs71M1e9Y
if (result == null) {
result = ex
} else {
result.initCause(null)
}
}
// A value of 0 means to not respond, and the rest is just an ID to match requests <-> responses
if (responseId == 0) {
return null
}
val invokeMethodResult = MethodResponse()
invokeMethodResult.objectId = methodRequest.objectId
invokeMethodResult.responseId = responseId.toByte()
invokeMethodResult.result = result
// logger.error("{} sent data: {} with id ({})", connection, result, invokeMethod.responseID);
return invokeMethodResult
}
}
// we start at 1, because 0 (INVALID_RMI) means we access connection only objects
private val rmiObjectIdCounter: AtomicInteger = if (isGlobal) {
AtomicInteger(0)
} else {
AtomicInteger(1)
}
// this is the ID -> Object RMI map. The RMI ID is used (not the kryo ID)
private val objectMap = LockFreeIntBiMap<Any>(INVALID_MAP_ID)
private fun nextObjectId(): Int {
// always increment by 2
// global RMI objects -> ODD in range 1-16380 (max 2 bytes) throws error on outside of range
// connection local RMI -> EVEN in range 1-16380 (max 2 bytes) throws error on outside of range
val value = rmiObjectIdCounter.getAndAdd(2)
if (value >= INVALID_RMI) {
rmiObjectIdCounter.set(INVALID_RMI) // prevent wrapping by spammy callers
logger.error("next RMI value '{}' has exceeded maximum limit '{}' in RmiBridge! Not creating RMI object.", value, INVALID_RMI)
return INVALID_RMI
}
return value
}
/**
* Automatically registers an object with the next available ID to allow the remote end of the RmiBridge connections to access it using the returned ID.
*
* @return the RMI object ID, if the registration failed (null object or TOO MANY objects), it will be [RmiServer.INVALID_RMI] (Integer.MAX_VALUE).
*/
fun register(`object`: Any?): Int {
if (`object` == null) {
return INVALID_RMI
}
// this will return INVALID_RMI if there are too many in the ObjectSpace
val nextObjectId = nextObjectId()
if (nextObjectId != INVALID_RMI) {
// specifically avoid calling register(int, Object) method to skip non-necessary checks + exceptions
objectMap.put(nextObjectId, `object`)
if (logger.isTraceEnabled) {
logger.trace("Object <proxy #{}> registered with ObjectSpace with .toString() = '{}'", nextObjectId, `object`)
}
}
return nextObjectId
}
/**
* Registers an object to allow the remote end of the RmiBridge connections to access it using the specified ID.
*
* @param objectID Must not be <0 or [RmiServer.INVALID_RMI] (Integer.MAX_VALUE).
*/
fun register(objectID: Int, `object`: Any?) {
require(objectID >= 0) { "objectID cannot be $INVALID_RMI" }
require(objectID < INVALID_RMI) { "objectID cannot be $INVALID_RMI" }
requireNotNull(`object`) { "object cannot be null." }
objectMap.put(objectID, `object`)
if (logger.isTraceEnabled) {
logger.trace("Object <proxy #{}> registered in RmiBridge with .toString() = '{}'", objectID, `object`)
}
}
/**
* Removes an object. The remote end of the RmiBridge connection will no longer be able to access it.
*/
fun remove(objectID: Int) {
val `object` = objectMap.remove(objectID)
if (logger.isTraceEnabled) {
logger.trace("Object <proxy #{}> removed from ObjectSpace with .toString() = '{}'", objectID, `object`)
}
}
/**
* Removes an object. The remote end of the RmiBridge connection will no longer be able to access it.
*/
fun remove(`object`: Any) {
val objectID = objectMap.inverse().remove(`object`)
if (objectID == INVALID_MAP_ID) {
logger.error("Object {} could not be found in the ObjectSpace.", `object`)
} else if (logger.isTraceEnabled) {
logger.trace("Object {} (ID: {}) removed from ObjectSpace.", `object`, objectID)
}
}
/**
* Returns the object registered with the specified ID.
*/
fun getRegisteredObject(objectID: Int): Any? {
return if (objectID < 0 || objectID >= INVALID_RMI) {
null
} else objectMap[objectID]
// Find an object with the objectID.
}
/**
* Returns the ID registered for the specified object, or INVALID_RMI if not found.
*/
fun <T> getRegisteredId(`object`: T): Int {
// Find an ID with the object.
val i = objectMap.inverse()[`object`]
return if (i == INVALID_MAP_ID) {
INVALID_RMI
} else i
}
}

View File

@ -0,0 +1,354 @@
/*
* Copyright 2019 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.rmi
import dorkbox.network.connection.Connection
import dorkbox.network.connection.Connection_
import dorkbox.network.connection.EndPoint
import dorkbox.network.rmi.messages.*
import dorkbox.network.serialization.NetworkSerializationManager
import dorkbox.util.classes.ClassHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mu.KLogger
import java.lang.reflect.Proxy
import java.util.*
class RmiSupport<C : Connection>(logger: KLogger,
actionDispatch: CoroutineScope,
internal val serialization: NetworkSerializationManager) : RmiSupportCache(logger, actionDispatch)
{
companion object {
/**
* Returns a proxy object that implements the specified interface, and the methods invoked on the proxy object will be invoked
* remotely.
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the [RemoteObject.responseTimeout].
*
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side will
* have the proxy object replaced with the registered object.
*
* @see RemoteObject
*
* @param rmiId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID
* @param interfaceClass this is the RMI interface class
*/
internal fun createProxyObject(isGlobalObject: Boolean,
connection: Connection, serialization: NetworkSerializationManager,
rmiSupportCache: RmiSupportCache, namePrefix: String,
rmiId: Int, interfaceClass: Class<*>): RemoteObject {
require(interfaceClass.isInterface) { "iface must be an interface." }
// duplicates are fine, as they represent the same object (as specified by the ID) on the remote side.
val classId = serialization.getClassId(interfaceClass)
val cachedMethods = serialization.getMethods(classId)
val name = "<${namePrefix}-proxy #$rmiId>"
// the ACTUAL proxy is created in the connection impl. Our proxy handler MUST BE suspending because of:
// 1) how we send data on the wire
// 2) how we must (sometimes) wait for a response
val proxyObject = RmiClient(isGlobalObject, rmiId, connection, name, rmiSupportCache, cachedMethods)
// This is the interface inheritance by the proxy object
val interfaces: Array<Class<*>> = arrayOf(RemoteObject::class.java, interfaceClass)
return Proxy.newProxyInstance(RmiSupport::class.java.classLoader, interfaces, proxyObject) as RemoteObject
}
/**
* Scans a class (+hierarchy) for @Rmi annotation and executes the 'registerAction' with it
*/
internal fun scanImplForRmiFields(logger: KLogger, implObject: Any, registerAction: (fieldObject: Any) -> Unit) {
val implementationClass = implObject::class.java
// the @Rmi annotation allows an RMI object to have fields with objects that are ALSO RMI objects
val classesToCheck = LinkedList<Map.Entry<Class<*>, Any?>>()
classesToCheck.add(AbstractMap.SimpleEntry(implementationClass, implObject))
var remoteClassObject: Map.Entry<Class<*>, Any?>
while (!classesToCheck.isEmpty()) {
remoteClassObject = classesToCheck.removeFirst()
// we have to check the IMPLEMENTATION for any additional fields that will have proxy information.
// we use getDeclaredFields() + walking the object hierarchy, so we get ALL the fields possible (public + private).
for (field in remoteClassObject.key.declaredFields) {
if (field.getAnnotation(Rmi::class.java) != null) {
val type = field.type
if (!type.isInterface) {
// the type must be an interface, otherwise RMI cannot create a proxy object
logger.error("Error checking RMI fields for: {}.{} -- It is not an interface!",
remoteClassObject.key,
field.name)
continue
}
//TODO FIX THIS. MAYBE USE KOTLIN TO DO THIS?
val prev = field.isAccessible
field.isAccessible = true
val o: Any
try {
o = field[remoteClassObject.value]
registerAction(o)
classesToCheck.add(AbstractMap.SimpleEntry(type, o))
} catch (e: IllegalAccessException) {
logger.error("Error checking RMI fields for: {}.{}", remoteClassObject.key, field.name, e)
} finally {
field.isAccessible = prev
}
}
}
// have to check the object hierarchy as well
val superclass = remoteClassObject.key.superclass
if (superclass != null && superclass != Any::class.java) {
classesToCheck.add(AbstractMap.SimpleEntry(superclass, remoteClassObject.value))
}
}
}
/**
* called on "client"
*/
private fun onGenericObjectResponse(endPoint: EndPoint<Connection_>, connection: Connection_, logger: KLogger,
isGlobal: Boolean, rmiId: Int, callback: suspend (Any) -> Unit,
rmiSupportCache: RmiSupportCache, serialization: NetworkSerializationManager) {
// we only create the proxy + execute the callback if the RMI id is valid!
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
logger.error {
"RMI ID '${rmiId}' is invalid. Unable to create RMI object on server."
}
return
}
val interfaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(RemoteObjectCallback::class.java, callback.javaClass, 0)
// create the client-side proxy object, if possible
var proxyObject = rmiSupportCache.getProxyObject(rmiId)
if (proxyObject == null) {
proxyObject = createProxyObject(isGlobal, connection, serialization, rmiSupportCache, endPoint.type.simpleName, rmiId, interfaceClass)
rmiSupportCache.saveProxyObject(rmiId, proxyObject)
}
// this should be executed on a NEW coroutine!
endPoint.actionDispatch.launch {
try {
callback(proxyObject)
} catch (e: Exception) {
logger.error("Error getting or creating the remote object $interfaceClass", e)
}
}
}
}
// this is used for all connection specific ones as well.
private val remoteObjectCreationCallbacks = RemoteObjectStorage(logger)
internal fun <Iface> registerCallback(callback: suspend (Iface) -> Unit): Int {
return remoteObjectCreationCallbacks.register(callback)
}
private fun removeCallback(callbackId: Int): suspend (Any) -> Unit {
return remoteObjectCreationCallbacks.remove(callbackId)
}
/**
* Get's the implementation object based on if it is global, or not global
*/
fun getImplObject(isGlobal: Boolean, rmiObjectId: Int, connection: Connection_): Any {
return if (isGlobal) getImplObject(rmiObjectId) else connection.rmiSupport().getImplObject(rmiObjectId)
}
override fun close() {
super.close()
remoteObjectCreationCallbacks.close()
}
/**
* on the "client" to get a global remote object (that exists on the server)
*/
fun <Iface> getGlobalRemoteObject(connection: C, endPoint: EndPoint<C>, objectId: Int, interfaceClass: Class<Iface>): Iface {
// this immediately returns BECAUSE the object must have already been created on the server (this is why we specify the rmiId)!
// so we can just instantly create the proxy object (or get the cached one)
var proxyObject = getProxyObject(objectId)
if (proxyObject == null) {
proxyObject = createProxyObject(true, connection, serialization, this, endPoint.type.simpleName, objectId, interfaceClass)
saveProxyObject(objectId, proxyObject)
}
@Suppress("UNCHECKED_CAST")
return proxyObject as Iface
}
/**
* on the "client" to create a global remote object (that exists on the server)
*/
suspend fun <Iface> createGlobalRemoteObject(connection: Connection, interfaceClassId: Int, callback: suspend (Iface) -> Unit) {
val callbackId = registerCallback(callback)
// There is no rmiID yet, because we haven't created it!
val message = GlobalObjectCreateRequest(RmiUtils.packShorts(interfaceClassId, callbackId))
// We use a callback to notify us when the object is ready. We can't "create this on the fly" because we
// 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
connection.send(message)
}
/**
* Manages ALL OF THE RMI stuff!
*/
@Throws(IllegalArgumentException::class)
suspend fun manage(endPoint: EndPoint<Connection_>, connection: Connection_, message: Any, logger: KLogger) {
when (message) {
is ConnectionObjectCreateRequest -> {
/**
* called on "server"
*/
connection.rmiSupport().onConnectionObjectCreateRequest(endPoint, connection, message, logger)
}
is ConnectionObjectCreateResponse -> {
/**
* called on "client"
*/
val rmiId = RmiUtils.unpackLeft(message.packedIds)
val callbackId = RmiUtils.unpackRight(message.packedIds)
val callback = removeCallback(callbackId)
onGenericObjectResponse(endPoint, connection, logger, false, rmiId, callback, this, serialization)
}
is GlobalObjectCreateRequest -> {
/**
* called on "server"
*/
onGlobalObjectCreateRequest(endPoint, connection, message, logger)
}
is GlobalObjectCreateResponse -> {
/**
* called on "client"
*/
val rmiId = RmiUtils.unpackLeft(message.packedIds)
val callbackId = RmiUtils.unpackRight(message.packedIds)
val callback = removeCallback(callbackId)
onGenericObjectResponse(endPoint, connection, logger, true, rmiId, callback, this, serialization)
}
is MethodRequest -> {
/**
* Invokes the method on the object and, sends the result back to the connection that made the invocation request.
*
* This is the response to the invoke method in the RmiClient
*
* The remote side of this connection requested the invocation.
*/
val objectId: Int = message.objectId
val isGlobal: Boolean = message.isGlobal
val cachedMethod = message.cachedMethod
val implObject = getImplObject(isGlobal, objectId, connection)
logger.trace {
var argString = ""
if (message.args != null) {
argString = Arrays.deepToString(message.args)
argString = argString.substring(1, argString.length - 1)
}
val stringBuilder = StringBuilder(128)
stringBuilder.append(connection.toString())
.append(" received: ")
.append(implObject.javaClass.simpleName)
stringBuilder.append(":").append(objectId)
stringBuilder.append("#").append(cachedMethod.method.name)
stringBuilder.append("(").append(argString).append(")")
if (cachedMethod.overriddenMethod != null) {
// did we override our cached method? THIS IS NOT COMMON.
stringBuilder.append(" [Connection method override]")
}
stringBuilder.toString()
}
var result: Any?
try {
// args!! is safe to do here (even though it doesn't make sense)
result = cachedMethod.invoke(connection, implObject, message.args!!)
} catch (ex: Exception) {
logger.error("Error invoking method: ${cachedMethod.method.declaringClass.name}.${cachedMethod.method.name}", ex)
result = ex.cause
// added to prevent a stack overflow when references is false, (because 'cause' == "this").
// See:
// https://groups.google.com/forum/?fromgroups=#!topic/kryo-users/6PDs71M1e9Y
if (result == null) {
result = ex
} else {
result.initCause(null)
}
}
val invokeMethodResult = MethodResponse()
invokeMethodResult.objectId = objectId
invokeMethodResult.responseId = message.responseId
invokeMethodResult.result = result
connection.send(invokeMethodResult)
}
is MethodResponse -> {
// notify the pending proxy requests that we have a response!
getResponseStorage().onMessage(message)
}
}
}
/**
* called on "server"
*/
private suspend fun onGlobalObjectCreateRequest(
endPoint: EndPoint<Connection_>, connection: Connection_, message: GlobalObjectCreateRequest, logger: KLogger) {
val interfaceClassId = RmiUtils.unpackLeft(message.packedIds)
val callbackId = RmiUtils.unpackRight(message.packedIds)
// We have to lookup the iface, since the proxy object requires it
val implObject = endPoint.serialization.createRmiObject(interfaceClassId)
val rmiId = registerImplObject(implObject)
if (rmiId != RemoteObjectStorage.INVALID_RMI) {
// this means we could register this object.
// next, scan this object to see if there are any RMI fields
scanImplForRmiFields(logger, implObject) {
registerImplObject(implObject)
}
} else {
logger.error {
"Trying to create an RMI object with the INVALID_RMI id!!"
}
}
// we send the message ANYWAYS, because the client needs to know it did NOT succeed!
connection.send(GlobalObjectCreateResponse(RmiUtils.packShorts(rmiId, callbackId)))
}
}

View File

@ -0,0 +1,55 @@
package dorkbox.network.rmi
import dorkbox.util.collections.LockFreeIntMap
import kotlinx.coroutines.CoroutineScope
import mu.KLogger
/**
* Cache for implementation and proxy objects.
*
* The impl/proxy objects CANNOT be stored in the same data structure, because their IDs are not tied to the same ID source (and there
* would be conflicts in the data structure)
*/
open class RmiSupportCache(logger: KLogger, actionDispatch: CoroutineScope) {
private val responseStorage = RmiResponseStorage(actionDispatch)
private val implObjects = RemoteObjectStorage(logger)
private val proxyObjects = LockFreeIntMap<RemoteObject>()
fun registerImplObject(rmiObject: Any): Int {
return implObjects.register(rmiObject)
}
fun getImplObject(rmiId: Int): Any {
return implObjects[rmiId]
}
fun removeImplObject(rmiId: Int) {
implObjects.remove(rmiId) as Any
}
/**
* Removes a proxy object from the system
*/
fun removeProxyObject(rmiId: Int) {
proxyObjects.remove(rmiId)
}
fun getProxyObject(rmiId: Int): RemoteObject? {
return proxyObjects[rmiId]
}
fun saveProxyObject(rmiId: Int, remoteObject: RemoteObject) {
proxyObjects.put(rmiId, remoteObject)
}
fun getResponseStorage(): RmiResponseStorage {
return responseStorage
}
open fun close() {
implObjects.close()
proxyObjects.clear()
responseStorage.close()
}
}

View File

@ -0,0 +1,80 @@
package dorkbox.network.rmi
import dorkbox.network.connection.Connection
import dorkbox.network.connection.Connection_
import dorkbox.network.connection.EndPoint
import dorkbox.network.rmi.messages.ConnectionObjectCreateRequest
import dorkbox.network.rmi.messages.ConnectionObjectCreateResponse
import dorkbox.network.serialization.NetworkSerializationManager
import kotlinx.coroutines.CoroutineScope
import mu.KLogger
class RmiSupportConnection<C: Connection>(logger: KLogger,
private val rmiGlobalSupport: RmiSupport<out Connection>,
private val serialization: NetworkSerializationManager,
actionDispatch: CoroutineScope) : RmiSupportCache(logger, actionDispatch) {
/**
* on the "client" to get a connection-specific remote object (that exists on the server)
*/
fun <Iface> getRemoteObject(connection: Connection, endPoint: EndPoint<Connection_>, objectId: Int, interfaceClass: Class<Iface>): Iface {
// this immediately returns BECAUSE the object must have already been created on the server (this is why we specify the rmiId)!
// so we can just instantly create the proxy object (or get the cached one)
var proxyObject = getProxyObject(objectId)
if (proxyObject == null) {
proxyObject = RmiSupport.createProxyObject(false, connection, serialization, rmiGlobalSupport, endPoint.type.simpleName, objectId, interfaceClass)
saveProxyObject(objectId, proxyObject)
}
@Suppress("UNCHECKED_CAST")
return proxyObject as Iface
}
/**
* on the "client" to create a connection-specific remote object (that exists on the server)
*/
suspend fun <Iface> createRemoteObject(connection: C, interfaceClassId: Int, callback: suspend (Iface) -> Unit) {
val callbackId = rmiGlobalSupport.registerCallback(callback)
// There is no rmiID yet, because we haven't created it!
val message = ConnectionObjectCreateRequest(RmiUtils.packShorts(interfaceClassId, callbackId))
// We use a callback to notify us when the object is ready. We can't "create this on the fly" because we
// 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
connection.send(message)
}
/**
* called on "server"
*/
internal suspend fun onConnectionObjectCreateRequest(
endPoint: EndPoint<Connection_>, connection: Connection_, message: ConnectionObjectCreateRequest, logger: KLogger) {
val interfaceClassId = RmiUtils.unpackLeft(message.packedIds)
val callbackId = RmiUtils.unpackRight(message.packedIds)
// We have to lookup the iface, since the proxy object requires it
val implObject = endPoint.serialization.createRmiObject(interfaceClassId)
val rmiId = registerImplObject(implObject)
if (rmiId != RemoteObjectStorage.INVALID_RMI) {
// this means we could register this object.
// next, scan this object to see if there are any RMI fields
RmiSupport.scanImplForRmiFields(logger, implObject) {
registerImplObject(implObject)
}
} else {
logger.error {
"Trying to create an RMI object with the INVALID_RMI id!!"
}
}
// we send the message ANYWAYS, because the client needs to know it did NOT succeed!
connection.send(ConnectionObjectCreateResponse(RmiUtils.packShorts(rmiId, callbackId)))
}
}

View File

@ -402,4 +402,17 @@ object RmiUtils {
allClasses.remove(clazz)
return allClasses
}
private const val RIGHT = 0xFFFF
fun packShorts(left: Int, right: Int): Int {
return left shl 16 or (right and RIGHT)
}
fun unpackLeft(packedInt: Int): Int {
return packedInt ushr 16 // >>> operator 0-fills from left
}
fun unpackRight(packedInt: Int): Int {
return packedInt and RIGHT
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.rmi.messages
/**
* These use packed IDs, because both are REALLY shorts, but the JVM deals better with ints.
*
* @param interfaceClassId (LEFT) the Kryo interface class ID to create
* @param callbackId (RIGHT) to know which callback to use when the object is created
*/
data class ConnectionObjectCreateRequest(val packedIds: Int) : RmiMessage

View File

@ -1,9 +1,10 @@
/*
* Copyright 2019 dorkbox, llc.
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
@ -12,29 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.rmi;
package dorkbox.network.rmi.messages
/**
* These use packed IDs, because both are REALLY shorts, but the JVM deals better with ints.
*
* @param rmiId (LEFT) the Kryo interface class ID to create
* @param callbackId (RIGHT) to know which callback to use when the object is created
*/
public
class MessageWithTestCow {
public int number;
public String text;
private TestCow testCow;
private
MessageWithTestCow() {
// for kryo
}
public
MessageWithTestCow(final TestCow test) {
testCow = test;
}
public
TestCow getTestCow() {
return testCow;
}
}
data class ConnectionObjectCreateResponse(val packedIds: Int) : RmiMessage

View File

@ -1,67 +0,0 @@
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.rmi.messages
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.connection.KryoExtra
import dorkbox.network.rmi.RmiServer
/**
* This is required, because with RMI, it is possible that the IMPL and IFACE can have DIFFERENT class IDs, in which case, the "client" cannot read the correct
* objects (because the IMPL class might not be registered, or that ID might be registered to a different class)
*/
class DORequestSerializer : Serializer<DynamicObjectRequest>() {
override fun write(kryo: Kryo, output: Output, objectRequest: DynamicObjectRequest) {
output.writeInt(objectRequest.rmiId, true)
output.writeInt(objectRequest.callbackId, true)
var id = kryo.getRegistration(objectRequest.interfaceClass).id
output.writeInt(id, true)
id = if (objectRequest.remoteObject != null) {
val kryoExtra = kryo as KryoExtra
kryoExtra.rmiSupport.getRegisteredId(objectRequest.remoteObject)
} else {
// can be < 0 or >= RmiBridge.INVALID_RMI (Integer.MAX_VALUE)
-1
}
output.writeInt(id, false)
}
override fun read(kryo: Kryo, input: Input, implementationType: Class<out DynamicObjectRequest>): DynamicObjectRequest {
val rmiId = input.readInt(true)
val callbackId = input.readInt(true)
val interfaceClassId = input.readInt(true)
val remoteObjectId = input.readInt(false)
// // We have to lookup the iface, since the proxy object requires it
val iface = kryo.getRegistration(interfaceClassId).type
var remoteObject: Any? = null
if (remoteObjectId >= 0 && remoteObjectId < RmiServer.INVALID_RMI) {
val kryoExtra = kryo as KryoExtra
val connection = kryoExtra.connection
remoteObject = kryoExtra.rmiSupport.getProxyObject(connection, remoteObjectId, iface)
}
return DynamicObjectRequest(iface, rmiId, callbackId, remoteObject)
}
}

View File

@ -1,67 +0,0 @@
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.rmi.messages
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.connection.KryoExtra
import dorkbox.network.rmi.RmiServer
/**
* This is required, because with RMI, it is possible that the IMPL and IFACE can have DIFFERENT class IDs, in which case, the "client" cannot read the correct
* objects (because the IMPL class might not be registered, or that ID might be registered to a different class)
*/
class DOResponseSerializer : Serializer<DynamicObjectResponse>() {
override fun write(kryo: Kryo, output: Output, objectResponse: DynamicObjectResponse) {
output.writeInt(objectResponse.rmiId, true)
output.writeInt(objectResponse.callbackId, true)
var id = kryo.getRegistration(objectResponse.interfaceClass).id
output.writeInt(id, true)
id = if (objectResponse.remoteObject != null) {
val kryoExtra = kryo as KryoExtra
kryoExtra.rmiSupport.getRegisteredId(objectResponse.remoteObject)
} else {
// can be < 0 or >= RmiBridge.INVALID_RMI (Integer.MAX_VALUE)
-1
}
output.writeInt(id, false)
}
override fun read(kryo: Kryo, input: Input, implementationType: Class<out DynamicObjectResponse>): DynamicObjectResponse {
val rmiId = input.readInt(true)
val callbackId = input.readInt(true)
val interfaceClassId = input.readInt(true)
val remoteObjectId = input.readInt(false)
// // We have to lookup the iface, since the proxy object requires it
val iface = kryo.getRegistration(interfaceClassId).type
var remoteObject: Any? = null
if (remoteObjectId >= 0 && remoteObjectId < RmiServer.INVALID_RMI) {
val kryoExtra = kryo as KryoExtra
val connection = kryoExtra.connection
remoteObject = kryoExtra.rmiSupport.getProxyObject(connection, remoteObjectId, iface)
}
return DynamicObjectResponse(iface, rmiId, callbackId, remoteObject)
}
}

View File

@ -1,69 +0,0 @@
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.rmi.messages
/**
* Message specifically to register a class implementation for RMI
*/
class DynamicObjectRequest : RmiMessage {
/**
* this is null if there are problems creating an object on the remote side, otherwise it is non-null.
*/
var remoteObject: Any? = null
/**
* this is used to create a NEW rmi object on the REMOTE side (these are bound the to connection. They are NOT GLOBAL, ie: available on all connections)
*/
var interfaceClass: Class<*>
/**
* this is used to get specific, GLOBAL rmi objects (objects that are not bound to a single connection)
*/
var rmiId: Int
/**
* this is the callback ID assigned by the LOCAL side, to know WHICH RMI callback to call when we have a remote object available
*/
var callbackId: Int
/**
* When requesting a new or existing remote object
* SENT FROM "local" -> "remote"
*
* @param interfaceClass the class to create
* @param rmiId the RMI id to get from the REMOTE side
* @param callbackId the rmi callback ID on the LOCAL side, to know which callback to use
*/
constructor(interfaceClass: Class<*>, rmiId: Int, callbackId: Int) {
this.interfaceClass = interfaceClass
this.rmiId = rmiId
this.callbackId = callbackId
}
/**
* This is when we successfully created a new object (if there was an error, remoteObject is null)
* SENT FROM "remote" -> "local"
*
* @param callbackId the rmi callback ID on the LOCAL side, to know which callback to use
*/
constructor(interfaceClass: Class<*>, rmiId: Int, callbackId: Int, remoteObject: Any?) {
// this.isRequest = false
this.interfaceClass = interfaceClass
this.rmiId = rmiId
this.callbackId = callbackId
this.remoteObject = remoteObject
}
}

View File

@ -1,52 +0,0 @@
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.rmi.messages
/**
* This is when we successfully created a new object (if there was an error, remoteObject is null)
* SENT FROM "remote" -> "local"
*
* @param callbackId the rmi callback ID on the LOCAL side, to know which callback to use
*/
class DynamicObjectResponse(interfaceClass: Class<*>, rmiId: Int, callbackId: Int, remoteObject: Any?) : RmiMessage {
/**
* this is null if there are problems creating an object on the remote side, otherwise it is non-null.
*/
var remoteObject: Any? = null
/**
* this is used to create a NEW rmi object on the REMOTE side (these are bound the to connection. They are NOT GLOBAL, ie: available on all connections)
*/
var interfaceClass: Class<*>
/**
* this is used to get specific, GLOBAL rmi objects (objects that are not bound to a single connection)
*/
var rmiId: Int
/**
* this is the callback ID assigned by the LOCAL side, to know WHICH RMI callback to call when we have a remote object available
*/
var callbackId: Int
init {
this.interfaceClass = interfaceClass
this.rmiId = rmiId
this.callbackId = callbackId
this.remoteObject = remoteObject
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.rmi.messages
/**
* These use packed IDs, because both are REALLY shorts, but the JVM deals better with ints.
*
* @param interfaceClassId (LEFT) the Kryo interface class ID to create
* @param callbackId (RIGHT) to know which callback to use when the object is created
*/
data class GlobalObjectCreateRequest(val packedIds: Int) : RmiMessage

View File

@ -0,0 +1,25 @@
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.rmi.messages
/**
* These use packed IDs, because both are REALLY shorts, but the JVM deals better with ints.
*
* @param rmiId (LEFT) the Kryo interface class ID to create
* @param callbackId (RIGHT) to know which callback to use when the object is created
*/
data class GlobalObjectCreateResponse(val packedIds: Int) : RmiMessage

View File

@ -39,16 +39,23 @@ import dorkbox.network.rmi.CachedMethod
/**
* Internal message to invoke methods remotely.
*/
class MethodRequest internal constructor() : RmiMessage {
// the registered kryo ID for the object
var objectId = 0
class MethodRequest : RmiMessage {
// if this object was a global or connection specific object
var isGlobal: Boolean = false
// This class is NOT sent across the wire (but it's contents are!). We use a custom serializer to manage this.
// the registered kryo ID for the object
// NOTE: this is REALLY a short, but is represented as an int to make life easier. It is also packed with the responseId for serialization
var objectId: Int = 0
// A value of 0 means to not respond, otherwise it is an ID to match requests <-> responses
// NOTE: this is REALLY a short, but is represented as an int to make life easier. It is also packed with the objectId for serialization
var responseId: Int = 0
// This field is NOT sent across the wire (but some of it's contents are).
// We use a custom serializer to manage this because we have to ALSO be able to serialize the invocation arguments.
// NOTE: the info we serialze is REALLY a short, but is represented as an int to make life easier. It is also packed!
lateinit var cachedMethod: CachedMethod
// these are the arguments for executing the method
// these are the arguments for executing the method (they are serialized using the info from the cachedMethod field
var args: Array<Any>? = null
// A value of 0 means to not respond, and the rest is just an ID to match requests <-> responses
var responseId: Byte = 0
}

View File

@ -40,60 +40,78 @@ import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.connection.KryoExtra
import dorkbox.network.rmi.RmiUtils
import java.lang.reflect.Method
/**
* Internal message to invoke methods remotely.
*/
@Suppress("ConstantConditionIf")
class MethodRequestSerializer : Serializer<MethodRequest>() {
companion object {
private const val DEBUG = false
}
override fun write(kryo: Kryo, output: Output, methodRequest: MethodRequest) {
val method = methodRequest.cachedMethod
if (DEBUG) {
System.err.println("WRITING")
System.err.println(":: objectID " + methodRequest.objectId)
System.err.println(":: methodClassID " + methodRequest.cachedMethod.methodClassId)
System.err.println(":: methodIndex " + methodRequest.cachedMethod.methodIndex)
System.err.println(":: isGlobal ${methodRequest.isGlobal}")
System.err.println(":: objectID ${methodRequest.objectId}")
System.err.println(":: methodClassID ${method.methodClassId}")
System.err.println(":: methodIndex ${method.methodIndex}")
}
output.writeInt(methodRequest.objectId, true)
output.writeInt(methodRequest.cachedMethod.methodClassId, true)
output.writeByte(methodRequest.cachedMethod.methodIndex)
// we pack objectId + responseId into the same "int", since they are both really shorts (but are represented as ints to make
// working with them a lot easier
output.writeInt(RmiUtils.packShorts(methodRequest.objectId, methodRequest.responseId), true)
output.writeInt(RmiUtils.packShorts(method.methodClassId, method.methodIndex), true)
output.writeBoolean(methodRequest.isGlobal)
val serializers = methodRequest.cachedMethod.serializers
val length = serializers.size
val args = methodRequest.args
for (i in 0 until length) {
val serializer = serializers[i]
if (serializer != null) {
kryo.writeObjectOrNull(output, args!![i], serializer)
} else {
kryo.writeClassAndObject(output, args!![i])
val serializers = method.serializers
if (serializers.isNotEmpty()) {
val args = methodRequest.args!!
serializers.forEachIndexed { index, serializer ->
if (serializer != null) {
kryo.writeObjectOrNull(output, args[index], serializer)
} else {
kryo.writeClassAndObject(output, args[index])
}
}
}
output.writeByte(methodRequest.responseId)
}
@Suppress("UNCHECKED_CAST")
override fun read(kryo: Kryo, input: Input, type: Class<out MethodRequest>): MethodRequest {
val objectID = input.readInt(true)
val methodClassID = input.readInt(true)
val methodIndex = input.readByte()
val objectIdRmiId = input.readInt(true)
val objectId = RmiUtils.unpackLeft(objectIdRmiId)
val responseId = RmiUtils.unpackRight(objectIdRmiId)
val methodInfo = input.readInt(true)
val methodClassId = RmiUtils.unpackLeft(methodInfo)
val methodIndex = RmiUtils.unpackRight(methodInfo)
val isGlobal = input.readBoolean()
if (DEBUG) {
System.err.println("READING")
System.err.println(":: objectID $objectID")
System.err.println(":: methodClassID $methodClassID")
System.err.println(":: isGlobal $isGlobal")
System.err.println(":: objectID $objectId")
System.err.println(":: methodClassID $methodClassId")
System.err.println(":: methodIndex $methodIndex")
}
(kryo as KryoExtra)
val cachedMethod = try {
(kryo as KryoExtra).serializationManager.getMethods(methodClassID)[methodIndex.toInt()]
kryo.getMethods(methodClassId)[methodIndex]
} catch (ex: Exception) {
val methodClass = kryo.getRegistration(methodClassID).type
val methodClass = kryo.getRegistration(methodClassId).type
throw KryoException("Invalid method index " + methodIndex + " for class: " + methodClass.name)
}
@ -109,6 +127,8 @@ class MethodRequestSerializer : Serializer<MethodRequest>() {
// this is specifically when we override an interface method, with an implementation method + Connection parameter (@ index 0)
argStartIndex = 1
args = arrayOfNulls<Any>(serializers.size + 1) as Array<Any>
// we have to save the connection this happened on, so it can be part of the method invocation
args[0] = kryo.connection
} else {
method = cachedMethod.method
@ -118,28 +138,29 @@ class MethodRequestSerializer : Serializer<MethodRequest>() {
val parameterTypes = method.parameterTypes
// we don't start at 0 for the arguments, in case we have an overwritten method (in which case, the 1st arg is always "Connection.class")
var i = 0
val n = serializers.size
var j = argStartIndex
// we don't start at 0 for the arguments, in case we have an overwritten method, in which case, the 1st arg is always "Connection.class"
var index = 0
val size = serializers.size
var argStart = argStartIndex
while (i < n) {
val serializer = serializers[i]
while (index < size) {
val serializer = serializers[index]
if (serializer != null) {
args[j] = kryo.readObjectOrNull(input, parameterTypes[i], serializer)
args[argStart] = kryo.readObjectOrNull(input, parameterTypes[index], serializer)
} else {
args[j] = kryo.readClassAndObject(input)
args[argStart] = kryo.readClassAndObject(input)
}
i++
j++
index++
argStart++
}
val invokeMethod = MethodRequest()
invokeMethod.objectId = objectID
invokeMethod.isGlobal = isGlobal
invokeMethod.objectId = objectId
invokeMethod.cachedMethod = cachedMethod
invokeMethod.args = args
invokeMethod.responseId = input.readByte()
invokeMethod.responseId = responseId
return invokeMethod
}

View File

@ -38,12 +38,14 @@ package dorkbox.network.rmi.messages
* Internal message to return the result of a remotely invoked method.
*/
class MethodResponse : RmiMessage {
// the registered kryo ID for the object
var objectId = 0
// if this object was a global or connection specific object
var isGlobal: Boolean = false
// A value of 0 means to not respond (this object is NOT created if the request 'responseId = 0'
// This is just an ID to match requests <-> responses
var responseId: Byte = 0
// the registered kryo ID for the object
var objectId: Int = 0
// ID to match requests <-> responses
var responseId: Int = 0
// this is the result of the invoked method
var result: Any? = null

View File

@ -19,18 +19,22 @@ import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.rmi.RmiUtils
class MethodResponseSerializer() : Serializer<MethodResponse>() {
override fun write(kryo: Kryo, output: Output, methodResponse: MethodResponse) {
output.writeInt(methodResponse.objectId, true)
output.writeByte(methodResponse.responseId)
kryo.writeClassAndObject(output, methodResponse.result)
override fun write(kryo: Kryo, output: Output, response: MethodResponse) {
output.writeInt(RmiUtils.packShorts(response.objectId, response.responseId), true)
output.writeBoolean(response.isGlobal)
kryo.writeClassAndObject(output, response.result)
}
override fun read(kryo: Kryo, input: Input, type: Class<out MethodResponse>): MethodResponse {
val packedInfo = input.readInt(true)
val response = MethodResponse()
response.objectId = input.readInt(true)
response.responseId = input.readByte()
response.objectId = RmiUtils.unpackLeft(packedInfo)
response.responseId = RmiUtils.unpackRight(packedInfo)
response.isGlobal = input.readBoolean()
response.result = kryo.readClassAndObject(input)
return response

View File

@ -50,17 +50,19 @@ import dorkbox.network.connection.KryoExtra
class ObjectResponseSerializer(private val rmiImplToIface: IdentityMap<Class<*>, Class<*>>) : Serializer<Any>(false) {
override fun write(kryo: Kryo, output: Output, `object`: Any) {
val kryoExtra = kryo as KryoExtra
val id = kryoExtra.rmiSupport.getRegisteredId(`object`) //
output.writeInt(id, true)
// val id = kryoExtra.rmiSupport.getRegisteredId(`object`) //
// output.writeInt(id, true)
output.writeInt(0, true)
}
override fun read(kryo: Kryo, input: Input, implementationType: Class<*>): Any {
override fun read(kryo: Kryo, input: Input, implementationType: Class<*>): Any? {
val kryoExtra = kryo as KryoExtra
val objectID = input.readInt(true)
// We have to lookup the iface, since the proxy object requires it
val iface = rmiImplToIface.get(implementationType)
val connection = kryoExtra.connection
return kryoExtra.rmiSupport.getProxyObject(connection, objectID, iface)
// return kryoExtra.rmiSupport.getProxyObject(connection, objectID, iface)
return null
}
}

View File

@ -21,27 +21,25 @@ import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.connection.KryoExtra
import dorkbox.network.rmi.RmiClient
import org.slf4j.Logger
import java.lang.reflect.Proxy
/**
* this is to manage serializing proxy object objects across the wire
* this is to manage serializing proxy object objects across the wire...
* SO the server sends an RMI object, and the client reads an RMI object
*/
class ObjectRequestSerializer(private val logger: Logger) : Serializer<Any>() {
class RmiClientRequestSerializer : Serializer<Any>() {
override fun write(kryo: Kryo, output: Output, proxyObject: Any) {
val handler = Proxy.getInvocationHandler(proxyObject) as RmiClient
output.writeBoolean(handler.isGlobal)
output.writeInt(handler.rmiObjectId, true)
}
override fun read(kryo: Kryo, input: Input, type: Class<*>?): Any? {
val objectID = input.readInt(true)
val kryoExtra = kryo as KryoExtra
val isGlobal = input.readBoolean()
val objectId = input.readInt(true)
kryo as KryoExtra
val `object` = kryoExtra.rmiSupport.getImplementationObject(objectID)
if (`object` == null) {
logger.error("Unknown object ID in RMI ObjectSpace: {}", objectID)
}
return `object`
val connection = kryo.connection
return connection.endPoint().rmiSupport.getImplObject(isGlobal, objectId, connection)
}
}

View File

@ -141,13 +141,30 @@ interface NetworkSerializationManager : SerializationManager {
/**
* @return true if the remote kryo registration are the same as our own
*/
fun verifyKryoRegistration(bytes: ByteArray): Boolean
fun verifyKryoRegistration(clientBytes: ByteArray): Boolean
/**
* @return the details of all registration IDs -> Class name used by kryo
*/
fun getKryoRegistrationDetails(): ByteArray
/**
* Creates a NEW object implementation based on the KRYO interface ID.
*
* @return the corresponding implementation object
*/
fun createRmiObject(interfaceClassId: Int): Any
/**
* Returns the Kryo class registration ID
*/
fun getClassId(iFace: Class<*>): Int
/**
* Returns the Kryo class from a registration ID
*/
fun getClassFromId(interfaceClassId: Int): Class<*>
/**
* Gets the RMI implementation based on the specified interface
*

View File

@ -15,20 +15,15 @@
*/
package dorkbox.network.serialization
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Registration
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.SerializerFactory
import com.esotericsoftware.kryo.*
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy
import com.esotericsoftware.kryo.util.IdentityMap
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
import dorkbox.network.connection.Connection_
import dorkbox.network.connection.KryoExtra
import dorkbox.network.connection.ping.PingMessage
import dorkbox.network.rmi.CachedMethod
import dorkbox.network.rmi.NopRmiConnection
import dorkbox.network.rmi.RmiUtils
import dorkbox.network.rmi.messages.*
import dorkbox.objectPool.ObjectPool
@ -37,6 +32,7 @@ import dorkbox.util.OS
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import org.agrona.collections.Int2ObjectHashMap
import org.objenesis.instantiator.ObjectInstantiator
import org.objenesis.strategy.StdInstantiatorStrategy
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -70,6 +66,26 @@ class Serialization(references: Boolean,
companion object {
const val CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE = 400
private val UNMODIFIABLE_COLLECTION_SERIALIZERS: Array<Pair<Class<Any>, Serializer<Any>>>
init {
val unmodSerializers = mutableListOf<Pair<Class<Any>, Serializer<Any>>>()
// hacky way to register unmodifiable serializers. This MUST be done here, because we ONLY want internal objects created once
@Suppress("UNCHECKED_CAST")
val kryo: Kryo = object : Kryo() {
override fun register(type: Class<*>, serializer: Serializer<*>): Registration {
val type1 = type as Class<Any>
val serializer1 = serializer as Serializer<Any>
unmodSerializers.add(Pair(type1, serializer1))
return super.register(type, serializer)
}
}
UnmodifiableCollectionsSerializer.registerSerializers(kryo)
UNMODIFIABLE_COLLECTION_SERIALIZERS = unmodSerializers.toTypedArray()
// end hack
}
/**
* Additionally, this serialization manager will register the entire class+interface hierarchy for an object. If you want to specify a
@ -86,41 +102,9 @@ class Serialization(references: Boolean,
fun DEFAULT(references: Boolean = true, factory: SerializerFactory<*>? = null): Serialization {
val serialization = Serialization(references, factory)
// these are registered using the default serializers
serialization.register(String::class.java)
serialization.register(Array<String>::class.java)
serialization.register(IntArray::class.java)
serialization.register(ShortArray::class.java)
serialization.register(FloatArray::class.java)
serialization.register(DoubleArray::class.java)
serialization.register(LongArray::class.java)
serialization.register(ByteArray::class.java)
serialization.register(CharArray::class.java)
serialization.register(BooleanArray::class.java)
serialization.register(Array<Int>::class.java)
serialization.register(Array<Short>::class.java)
serialization.register(Array<Float>::class.java)
serialization.register(Array<Double>::class.java)
serialization.register(Array<Long>::class.java)
serialization.register(Array<Byte>::class.java)
serialization.register(Array<Char>::class.java)
serialization.register(Array<Boolean>::class.java)
serialization.register(Array<Any>::class.java)
serialization.register(Array<Array<Any>>::class.java)
serialization.register(Class::class.java)
// necessary for the transport of exceptions.
serialization.register(StackTraceElement::class.java)
serialization.register(Array<StackTraceElement>::class.java)
serialization.register(ControlMessage::class.java)
serialization.register(PingMessage::class.java) // TODO this is built into aeron!??!?!?!
// TODO: this is for diffie hellmen handshake stuff!
// serialization.register(IESParameters::class.java, IesParametersSerializer())
// serialization.register(IESWithCipherParameters::class.java, IesWithCipherParametersSerializer())
@ -129,47 +113,15 @@ class Serialization(references: Boolean,
// serialization.register(XECPrivateKey::class.java, XECPrivateKeySerializer())
serialization.register(dorkbox.network.connection.registration.Registration::class.java) // must use full package name!
serialization.register(arrayListOf<Any>().javaClass)
serialization.register(hashMapOf<Any, Any>().javaClass)
serialization.register(hashSetOf<Any>().javaClass)
serialization.register(emptyList<Any>().javaClass)
serialization.register(emptySet<Any>().javaClass)
serialization.register(emptyMap<Any, Any>().javaClass)
serialization.register(Collections.EMPTY_LIST::class.java)
serialization.register(Collections.EMPTY_SET::class.java)
serialization.register(Collections.EMPTY_MAP::class.java)
serialization.register(Collections.emptyNavigableSet<Any>().javaClass)
serialization.register(Collections.emptyNavigableMap<Any, Any>().javaClass)
// hacky way to register unmodifiable serializers
@Suppress("UNCHECKED_CAST")
val kryo: Kryo = object : Kryo() {
override fun register(type: Class<*>, serializer: Serializer<*>): Registration {
val type1 = type as Class<Any>
val serializer1 = serializer as Serializer<Any>
serialization.register(type1, serializer1)
return super.register(type, serializer)
}
}
UnmodifiableCollectionsSerializer.registerSerializers(kryo)
// end hack
return serialization
}
// this prevents us from having to constantly do 'null' checks when serializing data
val NOP_CONNECTION: Connection_ = NopRmiConnection()
}
private lateinit var logger: Logger
private var initialized = false
private val kryoPool: ObjectPool<KryoExtra>
lateinit var classResolver: ClassResolver
// used by operations performed during kryo initialization, which are by default package access (since it's an anon-inner class)
// All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems.
@ -178,15 +130,26 @@ class Serialization(references: Boolean,
private lateinit var savedRegistrationDetails: ByteArray
/// RMI things
private val rmiIfaceToInstantiator : Int2ObjectHashMap<ObjectInstantiator<Any>> = Int2ObjectHashMap()
private val rmiIfaceToImpl = IdentityMap<Class<*>, Class<*>>()
private val rmiImplToIface = IdentityMap<Class<*>, Class<*>>()
private val remoteObjectSerializer = ObjectResponseSerializer(rmiImplToIface)
// BY DEFAULT, DefaultInstantiatorStrategy() will use ReflectASM
// StdInstantiatorStrategy will create classes bypasses the constructor (which can be useful in some cases) THIS IS A FALLBACK!
private val instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy())
private val methodRequestSerializer = MethodRequestSerializer()
private val methodResponseSerializer = MethodResponseSerializer()
private val objectRequestSerializer = RmiClientRequestSerializer()
private val objectResponseSerializer = ObjectResponseSerializer(rmiImplToIface)
// the purpose of the method cache, is to accelerate looking up methods for specific class
private val methodCache : Int2ObjectHashMap<Array<CachedMethod>> = Int2ObjectHashMap()
// reflectASM doesn't work on android
private val useAsm = !OS.isAndroid()
@ -196,33 +159,81 @@ class Serialization(references: Boolean,
synchronized(this@Serialization) {
// we HAVE to pre-allocate the KRYOs
val kryo = KryoExtra(this@Serialization)
val kryo = KryoExtra(methodCache)
// BY DEFAULT, DefaultInstantiatorStrategy() will use ReflectASM
// StdInstantiatorStrategy will create classes bypasses the constructor (which can be useful in some cases) THIS IS A FALLBACK!
kryo.instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy())
kryo.instantiatorStrategy = instantiatorStrategy
kryo.references = references
// All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems.
// these are registered using the default serializers. We don't customize these, because we don't care about it.
kryo.register(String::class.java)
kryo.register(Array<String>::class.java)
kryo.register(IntArray::class.java)
kryo.register(ShortArray::class.java)
kryo.register(FloatArray::class.java)
kryo.register(DoubleArray::class.java)
kryo.register(LongArray::class.java)
kryo.register(ByteArray::class.java)
kryo.register(CharArray::class.java)
kryo.register(BooleanArray::class.java)
kryo.register(Array<Int>::class.java)
kryo.register(Array<Short>::class.java)
kryo.register(Array<Float>::class.java)
kryo.register(Array<Double>::class.java)
kryo.register(Array<Long>::class.java)
kryo.register(Array<Byte>::class.java)
kryo.register(Array<Char>::class.java)
kryo.register(Array<Boolean>::class.java)
kryo.register(Array<Any>::class.java)
kryo.register(Array<Array<Any>>::class.java)
kryo.register(Class::class.java)
// necessary for the transport of exceptions.
kryo.register(StackTraceElement::class.java)
kryo.register(Array<StackTraceElement>::class.java)
kryo.register(arrayListOf<Any>().javaClass)
kryo.register(hashMapOf<Any, Any>().javaClass)
kryo.register(hashSetOf<Any>().javaClass)
kryo.register(emptyList<Any>().javaClass)
kryo.register(emptySet<Any>().javaClass)
kryo.register(emptyMap<Any, Any>().javaClass)
kryo.register(Collections.EMPTY_LIST::class.java)
kryo.register(Collections.EMPTY_SET::class.java)
kryo.register(Collections.EMPTY_MAP::class.java)
kryo.register(Collections.emptyNavigableSet<Any>().javaClass)
kryo.register(Collections.emptyNavigableMap<Any, Any>().javaClass)
UNMODIFIABLE_COLLECTION_SERIALIZERS.forEach {
kryo.register(it.first, it.second)
}
// RMI stuff!
kryo.register(GlobalObjectCreateRequest::class.java)
kryo.register(GlobalObjectCreateResponse::class.java)
kryo.register(ConnectionObjectCreateRequest::class.java)
kryo.register(ConnectionObjectCreateResponse::class.java)
kryo.register(MethodRequest::class.java, methodRequestSerializer)
kryo.register(MethodResponse::class.java, methodResponseSerializer)
@Suppress("UNCHECKED_CAST")
kryo.register(InvocationHandler::class.java as Class<Any>, objectRequestSerializer)
// check to see which interfaces are mapped to RMI (otherwise, the interface requires a serializer)
classesToRegister.forEach { registration ->
registration.register(kryo)
}
// RMI stuff. This has to be for each kryo instance!
// RMI stuff!
kryo.register(DynamicObjectRequest::class.java, DORequestSerializer())
kryo.register(DynamicObjectResponse::class.java, DOResponseSerializer())
kryo.register(MethodRequest::class.java, MethodRequestSerializer())
kryo.register(MethodResponse::class.java, MethodResponseSerializer())
@Suppress("UNCHECKED_CAST")
kryo.register(InvocationHandler::class.java as Class<Any>, ObjectRequestSerializer(logger))
if (factory != null) {
kryo.setDefaultSerializer(factory)
}
@ -340,12 +351,7 @@ class Serialization(references: Boolean,
/**
* There is additional overhead to using RMI.
*
* Specifically, It costs at least 2 bytes more to use remote method invocation than just sending the parameters. If the method has a
* return value which is not [ignored][dorkbox.network.rmi.RemoteObject.setAsync], an extra byte is written.
* If the type of a parameter is not final (primitives are final) then an extra byte is written for that parameter.
*
*
* Enable a "remote endpoint" to access methods and create objects (RMI) for this endpoint.
* This enables a "remote endpoint" to access methods and create objects (RMI) for this endpoint.
*
* This is NOT bi-directional, and this endpoint cannot access or create remote objects on the "remote client".
*
@ -361,7 +367,7 @@ class Serialization(references: Boolean,
require(ifaceClass.isInterface) { "Cannot register an implementation for RMI access. It must be an interface." }
require(!implClass.isInterface) { "Cannot register an interface for RMI implementations. It must be an implementation." }
classesToRegister.add(ClassRegistrationIfaceAndImpl(ifaceClass, implClass, remoteObjectSerializer))
classesToRegister.add(ClassRegistrationIfaceAndImpl(ifaceClass, implClass, objectResponseSerializer))
// rmiIfaceToImpl tells us, "the server" how to create a (requested) remote object
// this MUST BE UNIQUE otherwise unexpected and BAD things can happen.
@ -390,6 +396,10 @@ class Serialization(references: Boolean,
// 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)
val kryo = kryoPool.take()
// save off the class-resolver, so we can lookup the class <-> id relationships
classResolver = kryo.classResolver
try {
// now MERGE all of the registrations (since we can have registrations overwrite newer/specific registrations based on ID
// in order to get the ID's, these have to be registered with a kryo instance!
@ -451,18 +461,27 @@ class Serialization(references: Boolean,
// on the "RMI server" (aka, where the object lives) side, there will be an interface + implementation!
methodCache[classRegistration.id] =
RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, classRegistration.implClass, classRegistration.id)
// we ALSO have to cache the instantiator for these, since these are used to create remote objects
val instantiator = kryo.instantiatorStrategy.newInstantiatorOf(classRegistration.implClass)
@Suppress("UNCHECKED_CAST")
rmiIfaceToInstantiator[classRegistration.id] = instantiator as ObjectInstantiator<Any>
} else if (classRegistration.clazz.isInterface) {
// on the "RMI client"
methodCache[classRegistration.id] =
RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, null, classRegistration.id)
}
if (classRegistration.id > 65000) {
throw RuntimeException("There are too many kryo class registrations!!")
}
}
// save this as a byte array (so class registration validation during connection handshake is faster)
val buffer = Unpooled.buffer(CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE)
try {
kryo.writeCompressed(logger, NOP_CONNECTION, buffer, registrationDetails.toTypedArray())
kryo.writeCompressed(logger, buffer, registrationDetails.toTypedArray())
} catch (e: Exception) {
logger.error("Unable to write compressed data for registration details", e)
}
@ -494,25 +513,22 @@ class Serialization(references: Boolean,
*
* @return true if kryo registration is required for all classes sent over the wire
*/
override fun verifyKryoRegistration(bytes: ByteArray): Boolean {
val clientRegistrationData = bytes
override fun verifyKryoRegistration(clientBytes: ByteArray): Boolean {
// verify the registration IDs if necessary with our own. The CLIENT does not verify anything, only the server!
val kryoRegistrationDetails = savedRegistrationDetails
val equals = kryoRegistrationDetails.contentEquals(clientRegistrationData)
val equals = kryoRegistrationDetails.contentEquals(clientBytes)
if (equals) {
return true
}
// now we need to figure out WHAT was screwed up so we know what to fix
// NOTE: it could just be that the byte arrays are different, because java has a non-deterministic iteration of hash maps.
val kryo = takeKryo()
val byteBuf = Unpooled.wrappedBuffer(clientRegistrationData)
val byteBuf = Unpooled.wrappedBuffer(clientBytes)
try {
var success = true
@Suppress("UNCHECKED_CAST")
val clientClassRegistrations = kryo.readCompressed(logger, NOP_CONNECTION, byteBuf, clientRegistrationData.size) as Array<Array<Any>>
val clientClassRegistrations = kryo.readCompressed(logger, byteBuf, clientBytes.size) as Array<Array<Any>>
val lengthServer = classesToRegister.size
val lengthClient = clientClassRegistrations.size
var index = 0
@ -579,6 +595,31 @@ class Serialization(references: Boolean,
kryoPool.put(kryo)
}
/**
* Returns the Kryo class registration ID
*/
override fun getClassId(iFace: Class<*>): Int {
return classResolver.getRegistration(iFace).id
}
/**
* Returns the Kryo class from a registration ID
*/
override fun getClassFromId(interfaceClassId: Int): Class<*> {
return classResolver.getRegistration(interfaceClassId).type
}
/**
* Creates a NEW object implementation based on the KRYO interface ID.
*
* @return the corresponding implementation object
*/
override fun createRmiObject(interfaceClassId: Int): Any {
return rmiIfaceToInstantiator[interfaceClassId].newInstance()
}
/**
* Gets the RMI interface based on the specified implementation
*

View File

@ -1,287 +0,0 @@
/* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package dorkbox.network;
import static org.junit.Assert.fail;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import dorkbox.network.connection.EndPoint;
import dorkbox.util.entropy.Entropy;
import dorkbox.util.entropy.SimpleEntropy;
import dorkbox.util.exceptions.InitializationException;
import io.netty.util.ResourceLeakDetector;
public abstract
class BaseTest {
public static final String host = "127.0.0.1";
public static final int tcpPort = 54558;
public static final int udpPort = 54779;
// wait minimum of 2 minutes before we automatically fail the unit test.
public static final long AUTO_FAIL_TIMEOUT = 120;
private final Object lock = new Object();
private CountDownLatch latch = new CountDownLatch(1);
private volatile Thread autoFailThread = null;
static {
// disableAccessWarnings
try {
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Object unsafe = field.get(null);
Method putObjectVolatile = unsafeClass.getDeclaredMethod("putObjectVolatile", Object.class, long.class, Object.class);
Method staticFieldOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", Field.class);
Class loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");
Field loggerField = loggerClass.getDeclaredField("logger");
Long offset = (Long) staticFieldOffset.invoke(unsafe, loggerField);
putObjectVolatile.invoke(unsafe, loggerClass, offset, null);
}
catch (Exception ignored) {
}
// we want our entropy generation to be simple (ie, no user interaction to generate)
try {
Entropy.init(SimpleEntropy.class);
} catch (InitializationException e) {
e.printStackTrace();
}
}
private final List<EndPoint> endPointConnections = new CopyOnWriteArrayList<EndPoint>();
private volatile boolean isStopping = false;
public
BaseTest() {
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);
System.out.println("---- " + getClass().getSimpleName());
// assume SLF4J is bound to logback in the current environment
Logger rootLogger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
LoggerContext context = rootLogger.getLoggerContext();
JoranConfigurator jc = new JoranConfigurator();
jc.setContext(context);
context.reset(); // override default configuration
// rootLogger.setLevel(Level.OFF);
// rootLogger.setLevel(Level.INFO);
rootLogger.setLevel(Level.DEBUG);
// rootLogger.setLevel(Level.TRACE);
// rootLogger.setLevel(Level.ALL);
// we only want error messages
Logger nettyLogger = (Logger) LoggerFactory.getLogger("io.netty");
nettyLogger.setLevel(Level.ERROR);
// we only want error messages
Logger kryoLogger = (Logger) LoggerFactory.getLogger("com.esotericsoftware");
kryoLogger.setLevel(Level.ERROR);
// we only want error messages
Logger barchartLogger = (Logger) LoggerFactory.getLogger("com.barchart");
barchartLogger.setLevel(Level.ERROR);
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(context);
encoder.setPattern("%date{HH:mm:ss.SSS} %-5level [%logger{35}] %msg%n");
encoder.start();
ConsoleAppender<ILoggingEvent> consoleAppender = new ch.qos.logback.core.ConsoleAppender<ILoggingEvent>();
consoleAppender.setContext(context);
consoleAppender.setEncoder(encoder);
consoleAppender.start();
rootLogger.addAppender(consoleAppender);
}
public
void addEndPoint(final EndPoint endPointConnection) {
this.endPointConnections.add(endPointConnection);
synchronized (lock) {
latch = new CountDownLatch(endPointConnections.size() + 1);
}
}
/**
* Immediately stop the endpoints
*/
public
void stopEndPoints() {
stopEndPoints(0);
}
public
void stopEndPoints(final long stopAfterMillis) {
ThreadGroup threadGroup = Thread.currentThread()
.getThreadGroup();
final String name = threadGroup.getName();
// if (name.contains(THREADGROUP_NAME)) {
// // We have to ALWAYS run this in a new thread, BECAUSE if stopEndPoints() is called from a client/server thread, it will DEADLOCK
// final Thread thread = new Thread(threadGroup.getParent(), new Runnable() {
// @Override
// public
// void run() {
// stopEndPoints_(stopAfterMillis);
// }
// }, "UnitTest shutdown"); // a different name for the thread
//
// thread.setDaemon(true);
// thread.start();
// } else {
stopEndPoints_(stopAfterMillis);
// }
}
private
void stopEndPoints_(final long stopAfterMillis) {
if (isStopping) {
return;
}
isStopping = true;
// 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!
try {
Thread.sleep(stopAfterMillis);
} catch (InterruptedException ignored) {
}
synchronized (lock) {
}
// shutdown clients first
for (EndPoint endPoint : this.endPointConnections) {
if (endPoint.getType() == Client.class) {
endPoint.stop();
endPoint.waitForShutdown();
latch.countDown();
}
}
// shutdown servers last
for (EndPoint endPoint : this.endPointConnections) {
if (endPoint.getType() == Server.class) {
endPoint.stop();
endPoint.waitForShutdown();
latch.countDown();
}
}
// we start with "1", so make sure to end it
latch.countDown();
this.endPointConnections.clear();
}
/**
* 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() {
waitForThreads(AUTO_FAIL_TIMEOUT/2);
}
/**
* Wait for network client/server threads to shutdown for the specified time.
*
* @param stopAfterSeconds how many seconds to wait
*/
public
void waitForThreads(long stopAfterSeconds) {
synchronized (lock) {
}
try {
latch.await(stopAfterSeconds, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Before
public
void setupFailureCheck() {
autoFailThread = new Thread(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!
try {
Thread.sleep(AUTO_FAIL_TIMEOUT * 1000L);
// if the thread is interrupted, then it means we finished the test.
System.err.println("Test did not complete in a timely manner...");
stopEndPoints(0L);
fail("Test did not complete in a timely manner.");
} catch (InterruptedException ignored) {
}
}
}, "UnitTest timeout fail condition");
autoFailThread.setDaemon(true);
// autoFailThread.start();
}
@After
public
void cancelFailureCheck() {
if (autoFailThread != null) {
autoFailThread.interrupt();
autoFailThread = null;
}
// Give sockets a chance to close before starting the next test.
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
}
}

View File

@ -1,294 +0,0 @@
/* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package dorkbox.network;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Listener;
import dorkbox.network.connection.Listeners;
import dorkbox.network.connection.wrapper.ChannelWrapper;
import dorkbox.util.exceptions.InitializationException;
import dorkbox.util.exceptions.SecurityException;
public
class ListenerTest extends BaseTest {
private final String origString = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; // lots of a's to encourage compression
private final int limit = 20;
private AtomicInteger count = new AtomicInteger(0);
AtomicBoolean checkFail1 = new AtomicBoolean(false);
AtomicBoolean checkFail2 = new AtomicBoolean(false);
AtomicBoolean check1 = new AtomicBoolean(false);
AtomicBoolean check2 = new AtomicBoolean(false);
AtomicBoolean check3 = new AtomicBoolean(false);
AtomicBoolean check4 = new AtomicBoolean(false);
AtomicBoolean check5 = new AtomicBoolean(false);
AtomicBoolean check6 = new AtomicBoolean(false);
AtomicBoolean check7 = new AtomicBoolean(false);
AtomicBoolean check8 = new AtomicBoolean(false);
AtomicBoolean check9 = new AtomicBoolean(false);
// quick and dirty test to also test connection sub-classing
class TestConnectionA extends ConnectionImpl {
public
TestConnectionA(final EndPoint endPointConnection, final ChannelWrapper wrapper) {
super(endPointConnection, wrapper);
}
public
void check() {
ListenerTest.this.check1.set(true);
}
}
class TestConnectionB extends TestConnectionA {
public
TestConnectionB(final EndPoint endPointConnection, final ChannelWrapper wrapper) {
super(endPointConnection, wrapper);
}
@Override
public
void check() {
ListenerTest.this.checkFail1.set(true);
}
}
abstract class SubListener implements Listener.OnMessageReceived<Connection, String> {
}
abstract class SubListener2 extends SubListener {
}
abstract class SubListener3 implements Listener.OnMessageReceived<Connection, String>, Listener.SelfDefinedType {
}
@SuppressWarnings("rawtypes")
@Test
public
void listener() throws SecurityException, InitializationException, IOException, InterruptedException {
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
configuration.host = host;
Server server = new Server(configuration) {
@Override
public
TestConnectionA newConnection(final EndPoint endPoint, final ChannelWrapper wrapper) {
return new TestConnectionA(endPoint, wrapper);
}
};
addEndPoint(server);
server.bind(false);
final Listeners listeners = server.listeners();
// standard listener
listeners.add(new Listener.OnMessageReceived<TestConnectionA, String>() {
@Override
public
void received(TestConnectionA connection, String string) {
connection.check();
connection.send()
.TCP(string);
}
});
// standard listener with connection subclassed
listeners.add(new Listener.OnMessageReceived<Connection, String>() {
@Override
public
void received(Connection connection, String string) {
ListenerTest.this.check2.set(true);
}
});
// standard listener with message subclassed
listeners.add(new Listener.OnMessageReceived<TestConnectionA, Object>() {
@Override
public
void received(TestConnectionA connection, Object string) {
ListenerTest.this.check3.set(true);
}
});
// standard listener with connection subclassed AND message subclassed
listeners.add(new Listener.OnMessageReceived<Connection, Object>() {
@Override
public
void received(Connection connection, Object string) {
ListenerTest.this.check4.set(true);
}
});
// standard listener with connection subclassed AND message subclassed NO GENERICS
listeners.add(new Listener.OnMessageReceived() {
@Override
public
void received(Connection connection, Object string) {
ListenerTest.this.check5.set(true);
}
});
// subclassed listener with connection subclassed AND message subclassed NO GENERICS
listeners.add(new SubListener() {
@Override
public
void received(Connection connection, String string) {
ListenerTest.this.check6.set(true);
}
});
// subclassed listener with connection subclassed AND message subclassed NO GENERICS
listeners.add(new SubListener() {
@Override
public
void received(Connection connection, String string) {
ListenerTest.this.check6.set(true);
}
});
// subclassed listener with connection subclassed x 2 AND message subclassed NO GENERICS
listeners.add(new SubListener2() {
@Override
public
void received(Connection connection, String string) {
ListenerTest.this.check8.set(true);
}
});
// subclassed listener with connection subclassed AND message subclassed NO GENERICS
listeners.add(new SubListener3() {
@Override
public
Class<?> getType() {
return String.class;
}
@Override
public
void received(Connection connection, String string) {
ListenerTest.this.check9.set(true);
}
});
// standard listener disconnect check
listeners.add(new Listener.OnDisconnected<Connection>() {
@Override
public
void disconnected(Connection connection) {
ListenerTest.this.check7.set(true);
}
});
// should not let this happen!
try {
listeners.add(new Listener.OnMessageReceived<TestConnectionB, String>() {
@Override
public
void received(TestConnectionB connection, String string) {
connection.check();
System.err.println(string);
connection.TCP(string);
}
});
fail("Should not be able to ADD listeners that are NOT the basetype or the interface");
} catch (Exception e) {
System.err.println("Successfully did NOT add listener that was not the base class");
}
// ----
Client client = new Client(configuration);
addEndPoint(client);
client.listeners()
.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(Connection connection) {
connection.send()
.TCP(ListenerTest.this.origString); // 20 a's
}
});
client.listeners()
.add(new Listener.OnMessageReceived<Connection, String>() {
@Override
public
void received(Connection connection, String string) {
if (ListenerTest.this.count.getAndIncrement() < ListenerTest.this.limit) {
connection.send()
.TCP(string);
}
else {
if (!ListenerTest.this.origString.equals(string)) {
checkFail2.set(true);
System.err.println("original string not equal to the string received");
}
stopEndPoints();
}
}
});
client.connect(5000);
waitForThreads();
// -1 BECAUSE we are `getAndIncrement` for each check earlier
assertEquals(this.limit, this.count.get()-1);
assertTrue(this.check1.get());
assertTrue(this.check2.get());
assertTrue(this.check3.get());
assertTrue(this.check4.get());
assertTrue(this.check5.get());
assertTrue(this.check6.get());
assertTrue(this.check7.get());
assertTrue(this.check8.get());
assertTrue(this.check9.get());
assertFalse(this.checkFail1.get());
assertFalse(this.checkFail2.get());
}
}

View File

@ -1,129 +0,0 @@
/* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package dorkbox.network;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
import dorkbox.network.serialization.Serialization;
import dorkbox.util.exceptions.SecurityException;
public
class MultipleServerTest extends BaseTest {
AtomicInteger received = new AtomicInteger();
@Test
public
void multipleServers() throws SecurityException, IOException {
Configuration configuration1 = new Configuration();
configuration1.tcpPort = tcpPort;
configuration1.udpPort = udpPort;
configuration1.localChannelName = "chan1";
configuration1.serialization = Serialization.DEFAULT();
configuration1.serialization.register(String[].class);
Server server1 = new Server(configuration1);
addEndPoint(server1);
server1.bind(false);
server1.listeners()
.add(new Listener.OnMessageReceived<Connection, String>() {
@Override
public
void received(Connection connection, String object) {
if (!object.equals("client1")) {
fail();
}
if (MultipleServerTest.this.received.incrementAndGet() == 2) {
stopEndPoints();
}
}
});
Configuration configuration2 = new Configuration();
configuration2.tcpPort = tcpPort + 1;
configuration2.udpPort = udpPort + 1;
configuration2.localChannelName = "chan2";
configuration2.serialization = Serialization.DEFAULT();
configuration2.serialization.register(String[].class);
Server server2 = new Server(configuration2);
addEndPoint(server2);
server2.bind(false);
server2.listeners()
.add(new Listener.OnMessageReceived<Connection, String>() {
@Override
public
void received(Connection connection, String object) {
if (!object.equals("client2")) {
fail();
}
if (MultipleServerTest.this.received.incrementAndGet() == 2) {
stopEndPoints();
}
}
});
// ----
configuration1.localChannelName = null;
configuration1.host = host;
Client client1 = new Client(configuration1);
addEndPoint(client1);
client1.listeners()
.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(Connection connection) {
connection.send()
.TCP("client1");
}
});
client1.connect(5000);
configuration2.localChannelName = null;
configuration2.host = host;
Client client2 = new Client(configuration2);
addEndPoint(client2);
client2.listeners()
.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(Connection connection) {
connection.send()
.TCP("client2");
}
});
client2.connect(5000);
waitForThreads(30);
}
}

View File

@ -1,419 +0,0 @@
/* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package dorkbox.network;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Listener;
import dorkbox.network.connection.Listeners;
import dorkbox.network.serialization.Serialization;
import dorkbox.util.exceptions.SecurityException;
import dorkbox.util.serialization.SerializationManager;
public
class PingPongTest extends BaseTest {
private volatile String fail;
int tries = 1000;
enum TYPE {
TCP, UDP
}
@Test
public
void pingPong() throws SecurityException, IOException {
// UDP data is kinda big. Make sure it fits into one packet.
int origSize = EndPoint.udpMaxSize;
EndPoint.udpMaxSize = 2048;
this.fail = "Data not received.";
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
configuration.udpPort = udpPort;
configuration.host = host;
configuration.serialization = Serialization.DEFAULT();
register(configuration.serialization);
final Data dataTCP = new Data();
populateData(dataTCP, TYPE.TCP);
final Data dataUDP = new Data();
populateDataTiny(dataUDP, TYPE.UDP); // UDP has a max size it can send!
Server server = new Server(configuration);
addEndPoint(server);
server.bind(false);
final Listeners listeners1 = server.listeners();
listeners1.add(new Listener.OnError<Connection>() {
@Override
public
void error(Connection connection, Throwable throwable) {
PingPongTest.this.fail = "Error during processing. " + throwable;
}
});
listeners1.add(new Listener.OnMessageReceived<Connection, Data>() {
@Override
public
void received(Connection connection, Data data) {
if (data.type == TYPE.TCP) {
if (!data.equals(dataTCP)) {
PingPongTest.this.fail = "TCP data is not equal on server.";
throw new RuntimeException("Fail! " + PingPongTest.this.fail);
}
connection.send()
.TCP(dataTCP);
}
else if (data.type == TYPE.UDP) {
if (!data.equals(dataUDP)) {
PingPongTest.this.fail = "UDP data is not equal on server.";
throw new RuntimeException("Fail! " + PingPongTest.this.fail);
}
connection.send()
.UDP(dataUDP);
}
else {
PingPongTest.this.fail = "Unknown data type on server.";
throw new RuntimeException("Fail! " + PingPongTest.this.fail);
}
}
});
// ----
Client client = new Client(configuration);
addEndPoint(client);
final Listeners listeners = client.listeners();
listeners.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(Connection connection) {
PingPongTest.this.fail = null;
connection.send()
.TCP(dataTCP);
connection.send()
.UDP(dataUDP); // UDP ping pong stops if a UDP packet is lost.
}
});
listeners.add(new Listener.OnError<Connection>() {
@Override
public
void error(Connection connection, Throwable throwable) {
PingPongTest.this.fail = "Error during processing. " + throwable;
throwable.printStackTrace();
}
});
listeners.add(new Listener.OnMessageReceived<Connection, Data>() {
AtomicInteger checkTCP = new AtomicInteger(0);
AtomicInteger checkUDP = new AtomicInteger(0);
AtomicBoolean doneTCP = new AtomicBoolean(false);
AtomicBoolean doneUDP = new AtomicBoolean(false);
@Override
public
void received(Connection connection, Data data) {
if (data.type == TYPE.TCP) {
if (!data.equals(dataTCP)) {
PingPongTest.this.fail = "TCP data is not equal on client.";
throw new RuntimeException("Fail! " + PingPongTest.this.fail);
}
if (this.checkTCP.getAndIncrement() <= PingPongTest.this.tries) {
connection.send()
.TCP(dataTCP);
}
else {
System.err.println("TCP done.");
this.doneTCP.set(true);
}
}
else if (data.type == TYPE.UDP) {
if (!data.equals(dataUDP)) {
PingPongTest.this.fail = "UDP data is not equal on client.";
throw new RuntimeException("Fail! " + PingPongTest.this.fail);
}
if (this.checkUDP.getAndIncrement() <= PingPongTest.this.tries) {
connection.send()
.UDP(dataUDP);
}
else {
System.err.println("UDP done.");
this.doneUDP.set(true);
}
}
else {
PingPongTest.this.fail = "Unknown data type on client.";
throw new RuntimeException("Fail! " + PingPongTest.this.fail);
}
if (this.doneTCP.get() && this.doneUDP.get()) {
System.err.println("Ran TCP, UDP " + PingPongTest.this.tries + " times each");
stopEndPoints();
}
}
});
client.connect(5000);
waitForThreads();
if (this.fail != null) {
fail(this.fail);
}
EndPoint.udpMaxSize = origSize;
}
private static
void populateData(Data data, TYPE type) {
data.type = type;
StringBuilder buffer = new StringBuilder(3001);
for (int i = 0; i < 3000; i++) {
buffer.append('a');
}
data.string = buffer.toString();
data.strings = new String[] {"abcdefghijklmnopqrstuvwxyz0123456789", "", null, "!@#$", "<22><><EFBFBD><EFBFBD><EFBFBD>"};
data.ints = new int[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE};
data.shorts = new short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE};
data.floats = new float[] {0, -0, 1, -1, 123456, -123456, 0.1f, 0.2f, -0.3f, (float) Math.PI, Float.MAX_VALUE, Float.MIN_VALUE};
data.doubles = new double[] {0, -0, 1, -1, 123456, -123456, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE};
data.longs = new long[] {0, -0, 1, -1, 123456, -123456, 99999999999L, -99999999999L, Long.MAX_VALUE, Long.MIN_VALUE};
data.bytes = new byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE};
data.chars = new char[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE};
data.booleans = new boolean[] {true, false};
data.Ints = new Integer[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE};
data.Shorts = new Short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE};
data.Floats = new Float[] {0f, -0f, 1.0f, -1f, 123456f, -123456f, 0.1f, 0.2f, -0.3f, (float) Math.PI, Float.MAX_VALUE,
Float.MIN_VALUE};
data.Doubles = new Double[] {0d, -0d, 1d, -1d, 123456d, -123456d, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE};
data.Longs = new Long[] {0l, -0l, 1l, -1l, 123456l, -123456l, 99999999999l, -99999999999l, Long.MAX_VALUE, Long.MIN_VALUE};
data.Bytes = new Byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE};
data.Chars = new Character[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE};
data.Booleans = new Boolean[] {true, false};
}
// ONLY for UDP, since there is a 508 byte hard limit to UDP packets!
private static
void populateDataTiny(Data data, TYPE type) {
data.type = type;
StringBuilder buffer = new StringBuilder(10);
for (int i = 0; i < 10; i++) {
buffer.append('a');
}
data.string = buffer.toString();
data.strings = new String[] {"abcdefghijklmnopqrstuvwxyz0123456789", "", null, "!@#$", "<22><><EFBFBD><EFBFBD><EFBFBD>"};
data.ints = new int[] {Integer.MAX_VALUE, Integer.MIN_VALUE};
data.shorts = new short[] {Short.MAX_VALUE, Short.MIN_VALUE};
data.floats = new float[] {Float.MAX_VALUE, Float.MIN_VALUE};
data.doubles = new double[] {Double.MAX_VALUE, Double.MIN_VALUE};
data.longs = new long[] {Long.MAX_VALUE, Long.MIN_VALUE};
data.bytes = new byte[] {Byte.MAX_VALUE, Byte.MIN_VALUE};
data.chars = new char[] {Character.MAX_VALUE, Character.MIN_VALUE};
data.booleans = new boolean[] {true, false};
data.Ints = new Integer[] {Integer.MAX_VALUE, Integer.MIN_VALUE};
data.Shorts = new Short[] {Short.MAX_VALUE, Short.MIN_VALUE};
data.Floats = new Float[] {Float.MAX_VALUE, Float.MIN_VALUE};
data.Doubles = new Double[] {Double.MAX_VALUE, Double.MIN_VALUE};
data.Longs = new Long[] {Long.MAX_VALUE, Long.MIN_VALUE};
data.Bytes = new Byte[] {Byte.MAX_VALUE, Byte.MIN_VALUE};
data.Chars = new Character[] {Character.MAX_VALUE, Character.MIN_VALUE};
data.Booleans = new Boolean[] {true, false};
}
private
void register(SerializationManager manager) {
manager.register(int[].class);
manager.register(short[].class);
manager.register(float[].class);
manager.register(double[].class);
manager.register(long[].class);
manager.register(byte[].class);
manager.register(char[].class);
manager.register(boolean[].class);
manager.register(String[].class);
manager.register(Integer[].class);
manager.register(Short[].class);
manager.register(Float[].class);
manager.register(Double[].class);
manager.register(Long[].class);
manager.register(Byte[].class);
manager.register(Character[].class);
manager.register(Boolean[].class);
manager.register(Data.class);
manager.register(TYPE.class);
}
public static
class Data {
public TYPE type;
public String string;
public String[] strings;
public int[] ints;
public short[] shorts;
public float[] floats;
public double[] doubles;
public long[] longs;
public byte[] bytes;
public char[] chars;
public boolean[] booleans;
public Integer[] Ints;
public Short[] Shorts;
public Float[] Floats;
public Double[] Doubles;
public Long[] Longs;
public Byte[] Bytes;
public Character[] Chars;
public Boolean[] Booleans;
@Override
public
int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(this.Booleans);
result = prime * result + Arrays.hashCode(this.Bytes);
result = prime * result + Arrays.hashCode(this.Chars);
result = prime * result + Arrays.hashCode(this.Doubles);
result = prime * result + Arrays.hashCode(this.Floats);
result = prime * result + Arrays.hashCode(this.Ints);
result = prime * result + Arrays.hashCode(this.Longs);
result = prime * result + Arrays.hashCode(this.Shorts);
result = prime * result + Arrays.hashCode(this.booleans);
result = prime * result + Arrays.hashCode(this.bytes);
result = prime * result + Arrays.hashCode(this.chars);
result = prime * result + Arrays.hashCode(this.doubles);
result = prime * result + Arrays.hashCode(this.floats);
result = prime * result + Arrays.hashCode(this.ints);
result = prime * result + Arrays.hashCode(this.longs);
result = prime * result + Arrays.hashCode(this.shorts);
result = prime * result + (this.string == null ? 0 : this.string.hashCode());
result = prime * result + Arrays.hashCode(this.strings);
result = prime * result + (this.type == null ? 0 : this.type.hashCode());
return result;
}
@Override
public
boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Data other = (Data) obj;
if (!Arrays.equals(this.Booleans, other.Booleans)) {
return false;
}
if (!Arrays.equals(this.Bytes, other.Bytes)) {
return false;
}
if (!Arrays.equals(this.Chars, other.Chars)) {
return false;
}
if (!Arrays.equals(this.Doubles, other.Doubles)) {
return false;
}
if (!Arrays.equals(this.Floats, other.Floats)) {
return false;
}
if (!Arrays.equals(this.Ints, other.Ints)) {
return false;
}
if (!Arrays.equals(this.Longs, other.Longs)) {
return false;
}
if (!Arrays.equals(this.Shorts, other.Shorts)) {
return false;
}
if (!Arrays.equals(this.booleans, other.booleans)) {
return false;
}
if (!Arrays.equals(this.bytes, other.bytes)) {
return false;
}
if (!Arrays.equals(this.chars, other.chars)) {
return false;
}
if (!Arrays.equals(this.doubles, other.doubles)) {
return false;
}
if (!Arrays.equals(this.floats, other.floats)) {
return false;
}
if (!Arrays.equals(this.ints, other.ints)) {
return false;
}
if (!Arrays.equals(this.longs, other.longs)) {
return false;
}
if (!Arrays.equals(this.shorts, other.shorts)) {
return false;
}
if (this.string == null) {
if (other.string != null) {
return false;
}
}
else if (!this.string.equals(other.string)) {
return false;
}
if (!Arrays.equals(this.strings, other.strings)) {
return false;
}
if (this.type != other.type) {
return false;
}
return true;
}
@Override
public
String toString() {
return "Data";
}
}
}

View File

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

View File

@ -1,302 +0,0 @@
/*
* Copyright 2016 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.rmi;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import dorkbox.network.BaseTest;
import dorkbox.network.Client;
import dorkbox.network.Configuration;
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;
@SuppressWarnings("Duplicates")
public
class RmiSendObjectOverrideMethodTest extends BaseTest {
@Test
public
void rmiTcp() throws SecurityException, IOException {
rmi(new Config() {
@Override
public
void apply(final Configuration configuration) {
configuration.tcpPort = tcpPort;
configuration.host = host;
}
});
}
@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 {
rmi(new Config() {
@Override
public
void apply(final Configuration configuration) {
configuration.localChannelName = EndPoint.LOCAL_CHANNEL;
}
});
}
/**
* In this test the server has two objects in an object space. The client
* uses the first remote object to get the second remote object.
*
*
* The MAJOR difference in this version, is that we use an interface to override the methods, so that we can have the RMI system pass
* in the connection object.
*
* Specifically, from CachedMethod.java
*
* In situations where we want to pass in the Connection (to an RMI method), we have to be able to override method A, with method B.
* This is to support calling RMI methods from an interface (that does pass the connection reference) to
* an implType, that DOES pass the connection reference. The remote side (that initiates the RMI calls), MUST use
* the interface, and the implType may override the method, so that we add the connection as the first in
* the list of parameters.
*
* for example:
* Interface: foo(String x)
* Impl: foo(Connection c, String x)
*
* The implType (if it exists, with the same name, and with the same signature + connection parameter) will be called from the interface
* instead of the method that would NORMALLY be called.
*/
public
void rmi(final Config config) throws SecurityException, IOException {
Configuration configuration = new Configuration();
config.apply(configuration);
final boolean isUDP = configuration.udpPort > 0;
configuration.serialization = Serialization.DEFAULT();
configuration.serialization.registerRmi(TestObject.class, TestObjectImpl.class);
configuration.serialization.registerRmi(OtherObject.class, OtherObjectImpl.class);
configuration.serialization.register(OtherObjectImpl.class); // registered because this class is sent over the wire
Server server = new Server(configuration);
server.setIdleTimeout(0);
addEndPoint(server);
server.bind(false);
server.listeners()
.add(new Listener.OnMessageReceived<Connection, OtherObjectImpl>() {
@Override
public
void received(Connection connection, OtherObjectImpl object) {
// The test is complete when the client sends the OtherObject instance.
// this 'object' is the REAL object, not a proxy, because this object is created within this connection.
if (object.value() == 12.34f) {
stopEndPoints();
} else {
fail("Incorrect object value");
}
}
});
// ----
configuration = new Configuration();
config.apply(configuration);
configuration.serialization = Serialization.DEFAULT();
configuration.serialization.registerRmi(TestObject.class, TestObjectImpl.class);
configuration.serialization.registerRmi(OtherObject.class, OtherObjectImpl.class);
configuration.serialization.register(OtherObjectImpl.class); // registered because this class is sent over the wire
Client client = new Client(configuration);
client.setIdleTimeout(0);
addEndPoint(client);
client.listeners()
.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(final Connection connection) {
// if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work...
connection.createRemoteObject(TestObject.class, new RemoteObjectCallback<TestObject>() {
@Override
public
void created(final TestObject remoteObject) {
// MUST run on a separate thread because remote object method invocations are blocking
new Thread() {
@Override
public
void run() {
remoteObject.setOther(43.21f);
// Normal remote method call.
assertEquals(43.21f, remoteObject.other(), .0001f);
// Make a remote method call that returns another remote proxy object.
// the "test" object exists in the REMOTE side, as does the "OtherObject" that is created.
// here we have a proxy to both of them.
OtherObject otherObject = remoteObject.getOtherObject();
// Normal remote method call on the second object.
otherObject.setValue(12.34f);
float value = otherObject.value();
assertEquals(12.34f, value, .0001f);
// 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.
ConnectionBridge send = connection.send();
if (isUDP) {
send.UDP(otherObject)
.flush();
} else {
send.TCP(otherObject)
.flush();
}
}
}.start();
}
});
}
});
client.connect(5000);
waitForThreads();
}
private
interface TestObject {
void setOther(float aFloat);
float other();
OtherObject getOtherObject();
}
private
interface OtherObject {
void setValue(float aFloat);
float value();
}
private static final AtomicInteger idCounter = new AtomicInteger();
private static
class TestObjectImpl implements TestObject {
private final transient int ID = idCounter.getAndIncrement();
@Rmi
private final OtherObject otherObject = new OtherObjectImpl();
private float aFloat;
@Override
public
void setOther(final float aFloat) {
throw new RuntimeException("Whoops!");
}
public
void setOther(Connection connection, final float aFloat) {
this.aFloat = aFloat;
}
@Override
public
float other() {
throw new RuntimeException("Whoops!");
}
public
float other(Connection connection) {
return aFloat;
}
@Override
public
OtherObject getOtherObject() {
throw new RuntimeException("Whoops!");
}
public
OtherObject getOtherObject(Connection connection) {
return this.otherObject;
}
@Override
public
int hashCode() {
return ID;
}
}
private static
class OtherObjectImpl implements OtherObject {
private final transient int ID = idCounter.getAndIncrement();
private float aFloat;
@Override
public
void setValue(final float aFloat) {
this.aFloat = aFloat;
}
@Override
public
float value() {
return aFloat;
}
@Override
public
int hashCode() {
return ID;
}
}
}

View File

@ -1,259 +0,0 @@
/*
* Copyright 2016 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.
*
* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package dorkbox.network.rmi;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import dorkbox.network.BaseTest;
import dorkbox.network.Client;
import dorkbox.network.Configuration;
import dorkbox.network.Server;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Listener;
import dorkbox.network.serialization.Serialization;
import dorkbox.util.exceptions.SecurityException;
@SuppressWarnings("Duplicates")
public
class RmiSendObjectTest extends BaseTest {
@Test
public
void rmiNetwork() throws SecurityException, IOException {
rmi(new Config() {
@Override
public
void apply(final Configuration configuration) {
configuration.tcpPort = tcpPort;
configuration.host = host;
}
});
}
@Test
public
void rmiLocal() throws SecurityException, IOException {
rmi(new Config() {
@Override
public
void apply(final Configuration configuration) {
configuration.localChannelName = EndPoint.LOCAL_CHANNEL;
}
});
}
/**
* In this test the server has two objects in an object space. The client
* uses the first remote object to get the second remote object.
*/
public
void rmi(final Config config) throws SecurityException, IOException {
Configuration configuration = new Configuration();
config.apply(configuration);
configuration.serialization = Serialization.DEFAULT();
configuration.serialization.registerRmi(TestObject.class, TestObjectImpl.class);
configuration.serialization.registerRmi(OtherObject.class, OtherObjectImpl.class);
configuration.serialization.register(OtherObjectImpl.class); // registered because this class is sent over the wire
Server server = new Server(configuration);
server.setIdleTimeout(0);
addEndPoint(server);
server.bind(false);
server.listeners()
.add(new Listener.OnMessageReceived<Connection, OtherObjectImpl>() {
@Override
public
void received(Connection connection, OtherObjectImpl object) {
// The test is complete when the client sends the OtherObject instance.
if (object.value() == 12.34F) {
stopEndPoints();
} else {
fail("Incorrect object value");
}
}
});
// ----
configuration = new Configuration();
config.apply(configuration);
configuration.serialization = Serialization.DEFAULT();
configuration.serialization.registerRmi(TestObject.class, TestObjectImpl.class);
configuration.serialization.registerRmi(OtherObject.class, OtherObjectImpl.class);
configuration.serialization.register(OtherObjectImpl.class); // registered because this class is sent over the wire
Client client = new Client(configuration);
client.setIdleTimeout(0);
addEndPoint(client);
client.listeners()
.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(final Connection connection) {
connection.createRemoteObject(TestObject.class, new RemoteObjectCallback<TestObject>() {
@Override
public
void created(final TestObject remoteObject) {
// MUST run on a separate thread because remote object method invocations are blocking
new Thread() {
@Override
public
void run() {
remoteObject.setOther(43.21f);
// Normal remote method call.
assertEquals(43.21f, remoteObject.other(), 0.0001F);
// Make a remote method call that returns another remote proxy object.
OtherObject otherObject = remoteObject.getOtherObject();
// Normal remote method call on the second object.
otherObject.setValue(12.34f);
float value = otherObject.value();
assertEquals(12.34f, value, 0.0001F);
// 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)
.flush();
}
}.start();
}
});
}
});
client.connect(0);
waitForThreads();
}
private
interface TestObject {
void setOther(float aFloat);
float other();
OtherObject getOtherObject();
}
private
interface OtherObject {
void setValue(float aFloat);
float value();
}
private static final AtomicInteger idCounter = new AtomicInteger();
private static
class TestObjectImpl implements TestObject {
private final transient int ID = idCounter.getAndIncrement();
@Rmi
private final OtherObject otherObject = new OtherObjectImpl();
private float aFloat;
@Override
public
void setOther(final float aFloat) {
this.aFloat = aFloat;
}
@Override
public
float other() {
return aFloat;
}
@Override
public
OtherObject getOtherObject() {
return this.otherObject;
}
@Override
public
int hashCode() {
return ID;
}
}
private static
class OtherObjectImpl implements OtherObject {
private final transient int ID = idCounter.getAndIncrement();
private float aFloat;
@Override
public
void setValue(final float aFloat) {
this.aFloat = aFloat;
}
@Override
public
float value() {
return aFloat;
}
@Override
public
int hashCode() {
return ID;
}
}
}

View File

@ -1,358 +0,0 @@
/*
* Copyright 2016 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.
*
* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package dorkbox.network.rmi;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.junit.Test;
import dorkbox.network.BaseTest;
import dorkbox.network.Client;
import dorkbox.network.Configuration;
import dorkbox.network.Server;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Listener;
import dorkbox.network.connection.Listeners;
import dorkbox.network.serialization.NetworkSerializationManager;
import dorkbox.network.serialization.Serialization;
import dorkbox.util.exceptions.SecurityException;
public
class RmiTest extends BaseTest {
public static
void runTests(final Connection connection, final TestCow test, final int remoteObjectID) {
RemoteObject remoteObject = (RemoteObject) test;
// Default behavior. RMI is transparent, method calls behave like normal
// (return values and exceptions are returned, call is synchronous)
System.err.println("hashCode: " + test.hashCode());
System.err.println("toString: " + test.toString());
// see what the "remote" toString() method is
final String s = remoteObject.toString();
remoteObject.enableToString(true);
assertFalse(s.equals(remoteObject.toString()));
test.moo();
test.moo("Cow");
assertEquals(remoteObjectID, test.id());
// UDP calls that ignore the return value
remoteObject.setUDP();
remoteObject.setAsync(true);
remoteObject.setTransmitReturnValue(false);
remoteObject.setTransmitExceptions(false);
test.moo("Meow");
assertEquals(0, test.id());
remoteObject.setAsync(false);
remoteObject.setTransmitReturnValue(true);
remoteObject.setTransmitExceptions(true);
remoteObject.setTCP();
// Test that RMI correctly waits for the remotely invoked method to exit
remoteObject.setResponseTimeout(5000);
test.moo("You should see this two seconds before...", 2000);
System.out.println("...This");
remoteObject.setResponseTimeout(3000);
// Try exception handling
boolean caught = false;
try {
test.throwException();
} catch (UnsupportedOperationException ex) {
System.err.println("\tExpected.");
caught = true;
}
assertTrue(caught);
// Return values are ignored, but exceptions are still dealt with properly
remoteObject.setTransmitReturnValue(false);
test.moo("Baa");
test.id();
caught = false;
try {
test.throwException();
} catch (UnsupportedOperationException ex) {
caught = true;
}
assertTrue(caught);
// Non-blocking call that ignores the return value
remoteObject.setAsync(true);
remoteObject.setTransmitReturnValue(false);
test.moo("Meow");
assertEquals(0, test.id());
// Non-blocking call that returns the return value
remoteObject.setTransmitReturnValue(true);
test.moo("Foo");
assertEquals(0, test.id());
// wait for the response to id()
assertEquals(remoteObjectID, remoteObject.waitForLastResponse());
assertEquals(0, test.id());
byte responseID = remoteObject.getLastResponseID();
// wait for the response to id()
assertEquals(remoteObjectID, remoteObject.waitForResponse(responseID));
// Non-blocking call that errors out
remoteObject.setTransmitReturnValue(false);
test.throwException();
assertEquals(remoteObject.waitForLastResponse()
.getClass(), UnsupportedOperationException.class);
// Call will time out if non-blocking isn't working properly
remoteObject.setTransmitExceptions(false);
test.moo("Mooooooooo", 3000);
// should wait for a small time
remoteObject.setTransmitReturnValue(true);
remoteObject.setAsync(false);
remoteObject.setResponseTimeout(6000);
System.out.println("You should see this 2 seconds before");
float slow = test.slow();
System.out.println("...This");
assertEquals(slow, 123, 0.0001D);
// Test sending a reference to a remote object.
MessageWithTestCow m = new MessageWithTestCow(test);
m.number = 678;
m.text = "sometext";
connection.send()
.TCP(m)
.flush();
System.out.println("Finished tests");
}
public static
void register(NetworkSerializationManager manager) {
manager.register(Object.class); // Needed for Object#toString, hashCode, etc.
manager.register(TestCow.class);
manager.register(MessageWithTestCow.class);
manager.register(UnsupportedOperationException.class);
}
@Test
public
void rmiNetwork() throws SecurityException, IOException, InterruptedException {
rmi(new Config() {
@Override
public
void apply(final Configuration configuration) {
configuration.tcpPort = tcpPort;
configuration.udpPort = udpPort;
configuration.host = host;
}
});
// have to reset the object ID counter
TestCowImpl.ID_COUNTER.set(1);
Thread.sleep(2000L);
}
@Test
public
void rmiLocal() throws SecurityException, IOException, InterruptedException {
rmi(new Config() {
@Override
public
void apply(final Configuration configuration) {
configuration.localChannelName = EndPoint.LOCAL_CHANNEL;
}
});
// have to reset the object ID counter
TestCowImpl.ID_COUNTER.set(1);
Thread.sleep(2000L);
}
public
void rmi(final Config config) throws SecurityException, IOException {
Configuration configuration = new Configuration();
config.apply(configuration);
configuration.serialization = Serialization.DEFAULT();
register(configuration.serialization);
// for Client -> Server RMI
configuration.serialization.registerRmi(TestCow.class, TestCowImpl.class);
final Server server = new Server(configuration);
server.setIdleTimeout(0);
addEndPoint(server);
server.bind(false);
final Listeners listeners = server.listeners();
listeners.add(new Listener.OnMessageReceived<Connection, MessageWithTestCow>() {
@Override
public
void received(final Connection connection, MessageWithTestCow m) {
System.err.println("Received finish signal for test for: Client -> Server");
TestCow object = m.getTestCow();
final int id = object.id();
assertEquals(1, id);
System.err.println("Finished test for: Client -> Server");
System.err.println("Starting test for: Server -> Client");
// normally this is in the 'connected', but we do it here, so that it's more linear and easier to debug
// if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work...
connection.createRemoteObject(TestCow.class, new RemoteObjectCallback<TestCow>() {
@Override
public
void created(final TestCow remoteObject) {
// MUST run on a separate thread because remote object method invocations are blocking
new Thread() {
@Override
public
void run() {
System.err.println("Running test for: Server -> Client");
runTests(connection, remoteObject, 2);
System.err.println("Done with test for: Server -> Client");
}
}.start();
}
});
}
});
// ----
configuration = new Configuration();
config.apply(configuration);
configuration.serialization = Serialization.DEFAULT();
register(configuration.serialization);
// this is for testing the "screwed up registrations logic". It should screwup for both network AND local-JVM connections
// configuration.serialization.register(ExtraClassTest1.class);
// // for Server -> Client RMI
configuration.serialization.registerRmi(TestCow.class, TestCowImpl.class);
final Client client = new Client(configuration);
client.setIdleTimeout(0);
addEndPoint(client);
client.listeners().add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(final Connection connection) {
System.err.println("Starting test for: Client -> Server");
// if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work...
connection.createRemoteObject(TestCow.class, new RemoteObjectCallback<TestCow>() {
@Override
public
void created(final TestCow remoteObject) {
// MUST run on a separate thread because remote object method invocations are blocking
new Thread() {
@Override
public
void run() {
System.err.println("Running test for: Client -> Server");
runTests(connection, remoteObject, 1);
System.err.println("Done with test for: Client -> Server");
}
}.start();
}
});
}
});
client.listeners()
.add(new Listener.OnMessageReceived<Connection, MessageWithTestCow>() {
@Override
public
void received(Connection connection, MessageWithTestCow m) {
System.err.println("Received finish signal for test for: Client -> Server");
TestCow object = m.getTestCow();
final int id = object.id();
assertEquals(2, id);
System.err.println("Finished test for: Client -> Server");
stopEndPoints(2000);
}
});
client.connect(0);
waitForThreads();
}
private static
class ExtraClassTest1 {
int foo = 0;
public
ExtraClassTest1(final int foo) {
this.foo = foo;
}
}
private static
class ExtraClassTest2 {
int foo = 0;
public
ExtraClassTest2(final int foo) {
this.foo = foo;
}
}
}

View File

@ -1,105 +0,0 @@
/*
* Copyright 2019 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.rmi;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
*/
public
class TestCowImpl extends TestCowBaseImpl implements TestCow {
// has to start at 1, because UDP method invocations ignore return values
static final AtomicInteger ID_COUNTER = new AtomicInteger(1);
private int moos;
private final int id = ID_COUNTER.getAndIncrement();
public
TestCowImpl() {
}
@Override
public
void moo() {
this.moos++;
System.out.println("Moo! " + this.moos);
}
@Override
public
void moo(String value) {
this.moos += 2;
System.out.println("Moo! " + this.moos + ": " + value);
}
@Override
public
void moo(String value, long delay) {
this.moos += 4;
System.out.println("Moo! " + this.moos + ": " + value + " (" + delay + ")");
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public
int id() {
return id;
}
@Override
public
float slow() {
System.out.println("Slowdown!!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 123.0F;
}
@Override
public
boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final TestCowImpl that = (TestCowImpl) o;
return id == that.id;
}
@Override
public
int hashCode() {
return id;
}
@Override
public
String toString() {
return "Tada! This is a remote object!";
}
}

View File

@ -115,7 +115,8 @@ object AeronClient {
}
runBlocking {
client.connect("127.0.0.1")
// client.connect("127.0.0.1") // UDP connection via loopback
client.connect() // IPC connection
}

View File

@ -53,8 +53,9 @@ class RmiSendObjectOverrideMethodTest : BaseTest() {
}
/**
* In this test the server has two objects in an object space. The client
* uses the first remote object to get the second remote object.
* In this test the server has two objects in an object space.
*
* The client uses the first remote object to get the second remote object.
*
*
* The MAJOR difference in this version, is that we use an interface to override the methods, so that we can have the RMI system pass
@ -117,35 +118,35 @@ class RmiSendObjectOverrideMethodTest : BaseTest() {
client.onConnect { connection ->
// if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work...
connection.createRemoteObject(TestObject::class.java, object : RemoteObjectCallback<TestObject> {
override fun created(remoteObject: TestObject) {
// MUST run on a separate thread because remote object method invocations are blocking
object : Thread() {
override fun run() {
remoteObject.setOther(43.21f)
// Normal remote method call.
Assert.assertEquals(43.21f, remoteObject.other(), .0001f)
// Make a remote method call that returns another remote proxy object.
// the "test" object exists in the REMOTE side, as does the "OtherObject" that is created.
// here we have a proxy to both of them.
val otherObject = remoteObject.getOtherObject()
// Normal remote method call on the second object.
otherObject.setValue(12.34f)
val value = otherObject.value()
Assert.assertEquals(12.34f, value, .0001f)
// When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because
// that is where that object actually exists.
runBlocking {
connection.send(otherObject)
}
}
}.start()
}
})
// connection.create(TestObject::class.java, object : RemoteObjectCallback<TestObject> {
// override suspend fun created(remoteObject: TestObject) {
// // MUST run on a separate thread because remote object method invocations are blocking
// object : Thread() {
// override fun run() {
// remoteObject.setOther(43.21f)
//
// // Normal remote method call.
// Assert.assertEquals(43.21f, remoteObject.other(), .0001f)
//
// // Make a remote method call that returns another remote proxy object.
// // the "test" object exists in the REMOTE side, as does the "OtherObject" that is created.
// // here we have a proxy to both of them.
// val otherObject = remoteObject.getOtherObject()
//
// // Normal remote method call on the second object.
// otherObject.setValue(12.34f)
// val value = otherObject.value()
// Assert.assertEquals(12.34f, value, .0001f)
//
// // When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because
// // that is where that object actually exists.
// runBlocking {
// connection.send(otherObject)
// }
// }
// }.start()
// }
// })
}
runBlocking {

View File

@ -104,32 +104,32 @@ class RmiSendObjectTest : BaseTest() {
val client = Client<Connection>(configuration)
addEndPoint(client)
client.onConnect { connection ->
connection.createRemoteObject(TestObject::class.java, object : RemoteObjectCallback<TestObject> {
override fun created(remoteObject: TestObject) {
// MUST run on a separate thread because remote object method invocations are blocking
object : Thread() {
override fun run() {
remoteObject.setOther(43.21f)
// Normal remote method call.
Assert.assertEquals(43.21f, remoteObject.other(), 0.0001f)
// Make a remote method call that returns another remote proxy object.
val otherObject = remoteObject.getOtherObject()
// Normal remote method call on the second object.
otherObject.setValue(12.34f)
val value = otherObject.value()
Assert.assertEquals(12.34f, value, 0.0001f)
// When a remote proxy object is sent, the other side receives its actual remote object.
runBlocking {
connection.send(otherObject)
}
}
}.start()
}
})
// connection.create(TestObject::class.java, object : RemoteObjectCallback<TestObject> {
// override suspend fun created(remoteObject: TestObject) {
// // MUST run on a separate thread because remote object method invocations are blocking
// object : Thread() {
// override fun run() {
// remoteObject.setOther(43.21f)
//
// // Normal remote method call.
// Assert.assertEquals(43.21f, remoteObject.other(), 0.0001f)
//
// // Make a remote method call that returns another remote proxy object.
// val otherObject = remoteObject.getOtherObject()
//
// // Normal remote method call on the second object.
// otherObject.setValue(12.34f)
// val value = otherObject.value()
// Assert.assertEquals(12.34f, value, 0.0001f)
//
// // When a remote proxy object is sent, the other side receives its actual remote object.
// runBlocking {
// connection.send(otherObject)
// }
// }
// }.start()
// }
// })
}
runBlocking {

View File

@ -34,10 +34,7 @@
*/
package dorkbox.network.rmi
import dorkbox.network.BaseTest
import dorkbox.network.Client
import dorkbox.network.Configuration
import dorkbox.network.Server
import dorkbox.network.*
import dorkbox.network.connection.Connection
import dorkbox.network.rmi.classes.MessageWithTestCow
import dorkbox.network.rmi.classes.TestCow
@ -85,10 +82,22 @@ class RmiTest : BaseTest() {
Assert.assertTrue(caught)
// can ONLY wait for responses if we are ASYNC!
caught = false
try {
remoteObject.waitForLastResponse()
} catch (ex: IllegalStateException) {
caught = true
}
Assert.assertTrue(caught)
// Non-blocking call tests
// Non-blocking call tests
// Non-blocking call tests
remoteObject.setAsync(true)
System.err.println("I'm currently async: ${remoteObject.async}")
remoteObject.async = true
// calls that ignore the return value
@ -99,37 +108,43 @@ class RmiTest : BaseTest() {
// exceptions are still dealt with properly
test.moo("Baa")
test.id()
caught = false
try {
test.throwException()
} catch (ex: UnsupportedOperationException) {
} catch (ex: IllegalStateException) {
caught = true
}
Assert.assertTrue(caught)
remoteObject.setAsync(true)
// exceptions are not caught when async = true!
Assert.assertFalse(caught)
// wait for the response to id() EVEN THOUGH IT IS ASYNC?
Assert.assertEquals(remoteObjectID, remoteObject.waitForLastResponse())
Assert.assertEquals(0, test.id().toLong())
// now enable us to wait for responses
// can ONLY wait for responses if we are ASYNC + enabled waiting!!
remoteObject.enableWaitingForResponse(true)
val responseID = remoteObject.lastResponseID
test.id()
// wait for the response to id()
Assert.assertEquals(remoteObjectID, remoteObject.waitForResponse(responseID))
Assert.assertEquals(remoteObjectID, remoteObject.waitForLastResponse())
// wait for the response to id()
Assert.assertEquals(0, test.id().toLong())
val responseId = remoteObject.lastResponseId
Assert.assertEquals(remoteObjectID, remoteObject.waitForResponse(responseId))
// Non-blocking call that errors out
// remoteObject.setTransmitReturnValue(false)
test.throwException()
Assert.assertEquals(remoteObject.waitForLastResponse()?.javaClass, UnsupportedOperationException::class.java)
// Call will time out if non-blocking isn't working properly
// remoteObject.setTransmitExceptions(false)
test.moo("Mooooooooo", 3000)
test.moo("Mooooooooo", 4000)
// should wait for a small time
// remoteObject.setTransmitReturnValue(true)
remoteObject.setAsync(false)
remoteObject.async = false
remoteObject.responseTimeout = 6000
println("You should see this 2 seconds before")
val slow = test.slow()
@ -155,7 +170,17 @@ class RmiTest : BaseTest() {
@Test
@Throws(SecurityException::class, IOException::class, InterruptedException::class)
fun rmiNetwork() {
fun rmiNetworkGlobal() {
rmiGlobal()
// have to reset the object ID counter
TestCowImpl.ID_COUNTER.set(1)
Thread.sleep(2000L)
}
@Test
@Throws(SecurityException::class, IOException::class, InterruptedException::class)
fun rmiNetworkConnection() {
rmi()
// have to reset the object ID counter
@ -166,9 +191,10 @@ class RmiTest : BaseTest() {
@Test
@Throws(SecurityException::class, IOException::class, InterruptedException::class)
fun rmiIPC() {
TODO("DO IPC STUFF!")
rmi { configuration ->
// configuration.localChannelName = EndPoint.LOCAL_CHANNEL
if (configuration is ServerConfiguration) {
configuration.listenIpAddress = LOOPBACK
}
}
// have to reset the object ID counter
@ -200,21 +226,11 @@ class RmiTest : BaseTest() {
System.err.println("Starting test for: Server -> Client")
// normally this is in the 'connected', but we do it here, so that it's more linear and easier to debug
// if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work...
connection.createRemoteObject(TestCow::class.java, object : RemoteObjectCallback<TestCow> {
override fun created(remoteObject: TestCow) {
// MUST run on a separate thread because remote object method invocations are blocking
object : Thread() {
override fun run() {
System.err.println("Running test for: Server -> Client")
runBlocking {
runTests(connection, remoteObject, 2)
System.err.println("Done with test for: Server -> Client")
}
}
}.start()
}
})
connection.createObject<TestCow> { remoteObject ->
System.err.println("Running test for: Server -> Client")
runTests(connection, remoteObject, 2)
System.err.println("Done with test for: Server -> Client")
}
}
}
@ -223,10 +239,7 @@ class RmiTest : BaseTest() {
config(configuration)
register(configuration.serialization)
// this is for testing the "screwed up registrations logic". It should screwup for both network AND local-JVM connections
// configuration.serialization.register(ExtraClassTest1.class);
// // for Server -> Client RMI
// for Server -> Client RMI
configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java)
val client = Client<Connection>(configuration)
addEndPoint(client)
@ -234,24 +247,14 @@ class RmiTest : BaseTest() {
client.onConnect { connection ->
System.err.println("Starting test for: Client -> Server")
// if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work...
connection.createRemoteObject(TestCow::class.java, object : RemoteObjectCallback<TestCow> {
override fun created(remoteObject: TestCow) {
// MUST run on a separate thread because remote object method invocations are blocking
object : Thread() {
override fun run() {
System.err.println("Running test for: Client -> Server")
runBlocking {
runTests(connection, remoteObject, 1)
}
System.err.println("Done with test for: Client -> Server")
}
}.start()
}
})
connection.createObject<TestCow> { remoteObject ->
System.err.println("Running test for: Client -> Server")
runTests(connection, remoteObject, 1)
System.err.println("Done with test for: Client -> Server")
}
}
client.onMessage<MessageWithTestCow> { connection, m ->
client.onMessage<MessageWithTestCow> { _, m ->
System.err.println("Received finish signal for test for: Client -> Server")
val `object` = m.testCow
val id = `object`.id()
@ -268,19 +271,73 @@ class RmiTest : BaseTest() {
waitForThreads()
}
private class ExtraClassTest1(foo: Int) {
var foo = 0
@Throws(SecurityException::class, IOException::class)
fun rmiGlobal(config: (Configuration) -> Unit = {}) {
run {
val configuration = serverConfig()
config(configuration)
register(configuration.serialization)
init {
this.foo = foo
// for Client -> Server RMI
configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java)
val server = Server<Connection>(configuration)
addEndPoint(server)
server.bind(false)
server.onMessage<MessageWithTestCow> { connection, m ->
System.err.println("Received finish signal for test for: Client -> Server")
val `object` = m.testCow
val id = `object`.id()
Assert.assertEquals(1, id.toLong())
System.err.println("Finished test for: Client -> Server")
// normally this is in the 'connected', but we do it here, so that it's more linear and easier to debug
connection.createObject<TestCow> { remoteObject ->
System.err.println("Running test for: Server -> Client")
runTests(connection, remoteObject, 2)
System.err.println("Done with test for: Server -> Client")
}
}
}
}
private class ExtraClassTest2(foo: Int) {
var foo = 0
run {
val configuration = clientConfig()
config(configuration)
register(configuration.serialization)
init {
this.foo = foo
// for Server -> Client RMI
configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java)
val client = Client<Connection>(configuration)
addEndPoint(client)
client.onMessage<MessageWithTestCow> { _, m ->
System.err.println("Received finish signal for test for: Client -> Server")
val `object` = m.testCow
val id = `object`.id()
Assert.assertEquals(2, id.toLong())
System.err.println("Finished test for: Client -> Server")
stopEndPoints(2000)
}
runBlocking {
client.connect(LOOPBACK)
System.err.println("Starting test for: Client -> Server")
// this creates a GLOBAL object on the server (instead of a connection specific object)
client.createObject<TestCow> { remoteObject ->
System.err.println("Running test for: Client -> Server")
runTests(client.getConnection(), remoteObject, 1)
System.err.println("Done with test for: Client -> Server")
}
}
}
waitForThreads()
}
}

View File

@ -14,9 +14,6 @@
*/
package dorkbox.network.rmi.classes
/**
*
*/
class MessageWithTestCow(val testCow: TestCow) {
var number = 0
var text: String? = null

View File

@ -14,11 +14,9 @@
*/
package dorkbox.network.rmi.classes
/**
*
*/
open class TestCowBaseImpl : TestCowBase {
override fun throwException() {
System.err.println("The following exception is EXPECTED, but should only be on one log!")
throw UnsupportedOperationException("Why would I do that?")
}