WIP - finished global/connection specific RMI, IPC aeron connectivity
parent
fc7baa6c8d
commit
1608c0d6a2
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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 +
|
||||
|
|
|
@ -102,4 +102,9 @@ interface CoroutineIdleStrategy {
|
|||
fun alias(): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a clone of this IdleStrategy
|
||||
*/
|
||||
fun clone(): CoroutineIdleStrategy
|
||||
}
|
||||
|
|
|
@ -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 +
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]"
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
|
|
@ -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<*>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dorkbox.network;
|
||||
package dorkbox.network.other;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.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() }
|
||||
}
|
|
@ -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<*>?>) {
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!";
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,6 @@
|
|||
*/
|
||||
package dorkbox.network.rmi.classes
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class MessageWithTestCow(val testCow: TestCow) {
|
||||
var number = 0
|
||||
var text: String? = null
|
||||
|
|
|
@ -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?")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue