WIP - finished global/connection specific RMI, IPC aeron connectivity
This commit is contained in:
parent
fc7baa6c8d
commit
1608c0d6a2
|
@ -283,6 +283,9 @@ tasks.withType<KotlinCompile> {
|
||||||
jvmTarget = Extras.JAVA_VERSION
|
jvmTarget = Extras.JAVA_VERSION
|
||||||
apiVersion = Extras.KOTLIN_API_VERSION
|
apiVersion = Extras.KOTLIN_API_VERSION
|
||||||
languageVersion = Extras.KOTLIN_LANG_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:ObjectPool:2.12")
|
||||||
implementation("com.dorkbox:Utilities:1.5.3")
|
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("io.github.microutils:kotlin-logging:1.7.9") // slick kotlin wrapper for slf4j
|
||||||
implementation("org.slf4j:slf4j-api:1.7.30")
|
implementation("org.slf4j:slf4j-api:1.7.30")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13")
|
testImplementation("junit:junit:4.13")
|
||||||
testImplementation("ch.qos.logback:logback-classic:1.2.3")
|
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.ClientException
|
||||||
import dorkbox.network.aeron.client.ClientTimedOutException
|
import dorkbox.network.aeron.client.ClientTimedOutException
|
||||||
import dorkbox.network.connection.*
|
import dorkbox.network.connection.*
|
||||||
|
import dorkbox.network.other.NetUtil
|
||||||
|
import dorkbox.network.other.NetworkUtil
|
||||||
import dorkbox.util.exceptions.SecurityException
|
import dorkbox.util.exceptions.SecurityException
|
||||||
import kotlinx.atomicfu.atomic
|
import kotlinx.atomicfu.atomic
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -138,7 +140,7 @@ open class Client<C : Connection>(config: Configuration = Configuration()) : End
|
||||||
*/
|
*/
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
@Throws(IOException::class, ClientTimedOutException::class)
|
@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) {
|
if (isConnected.value) {
|
||||||
throw IOException("Unable to connect when already connected!");
|
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!")
|
throw IllegalArgumentException("0.0.0.0 is an invalid address to connect to!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectionManager.init(this)
|
||||||
|
|
||||||
// this is an IPC address
|
if (this.remoteAddress.isEmpty()) {
|
||||||
if (this.remoteAddress.startsWith("0x")) {
|
// this is an IPC address
|
||||||
val ipcAddress: Long
|
|
||||||
try {
|
|
||||||
ipcAddress = remoteAddress.toLong(radix = 16)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw IOException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
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(
|
val handshakeConnection = UdpMediaDriverConnection(
|
||||||
this.remoteAddress, config.publicationPort, config.subscriptionPort,
|
address = this.remoteAddress,
|
||||||
UDP_HANDSHAKE_STREAM_ID, RESERVED_SESSION_ID_INVALID,
|
subscriptionPort = config.publicationPort,
|
||||||
connectionTimeoutMS, reliable)
|
publicationPort = config.subscriptionPort,
|
||||||
|
streamId = UDP_HANDSHAKE_STREAM_ID,
|
||||||
|
sessionId = RESERVED_SESSION_ID_INVALID,
|
||||||
|
connectionTimeoutMS = connectionTimeoutMS,
|
||||||
|
isReliable = reliable)
|
||||||
|
|
||||||
closables.add(handshakeConnection)
|
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
|
// 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)
|
// @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
|
// we are now connected, so we can connect to the NEW client-specific ports
|
||||||
val reliableClientConnection = UdpMediaDriverConnection(handshakeConnection.address, connectionInfo.subscriptionPort, connectionInfo.publicationPort,
|
val reliableClientConnection = UdpMediaDriverConnection(
|
||||||
connectionInfo.streamId, connectionInfo.sessionId, connectionTimeoutMS, handshakeConnection.isReliable)
|
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!
|
// VALIDATE:: check to see if the remote connection's public key has changed!
|
||||||
if (!validateRemoteAddress(NetworkUtil.IP.toInt(this.remoteAddress), connectionInfo.publicKey)) {
|
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"
|
* Tells the remote connection to create a new global object that implements the specified interface.
|
||||||
* to an object that is created remotely.
|
|
||||||
*
|
|
||||||
*
|
*
|
||||||
|
* 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.
|
* The callback will be notified when the remote object has been created.
|
||||||
*
|
*
|
||||||
*
|
* 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.
|
||||||
*
|
|
||||||
* 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
|
* 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.
|
* 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.
|
* If one wishes to change the remote object behavior, cast the object [RemoteObject] to access the different methods, for example:
|
||||||
* ie: `RemoteObject remoteObject = (RemoteObject) test;`
|
* ie: `val remoteObject = test as RemoteObject`
|
||||||
*
|
*
|
||||||
* @see RemoteObject
|
* @see RemoteObject
|
||||||
*/
|
*/
|
||||||
// override fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>) {
|
suspend inline fun <reified Iface> createObject(noinline callback: suspend (Iface) -> Unit) {
|
||||||
// try {
|
val classId = serialization.getClassId(Iface::class.java)
|
||||||
// connection!!.createRemoteObject(interfaceClass, callback)
|
rmiSupport.createGlobalRemoteObject(getConnection(), classId, callback)
|
||||||
// } catch (e: NullPointerException) {
|
}
|
||||||
// logger.error("Error creating remote object!", e)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
|
* Gets a global remote object via the ID.
|
||||||
* to an object that is created remotely.
|
|
||||||
*
|
*
|
||||||
|
* 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.
|
* 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
|
* 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.
|
* will have the proxy object replaced with the registered (non-proxy) object.
|
||||||
*
|
*
|
||||||
*
|
* If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example:
|
||||||
* If one wishes to change the default behavior, cast the object to access the different methods.
|
* ie: `val remoteObject = test as RemoteObject`
|
||||||
* ie: `RemoteObject remoteObject = (RemoteObject) test;`
|
|
||||||
*
|
*
|
||||||
* @see RemoteObject
|
* @see RemoteObject
|
||||||
*/
|
*/
|
||||||
// override fun <Iface> getRemoteObject(objectId: Int, callback: RemoteObjectCallback<Iface>) {
|
fun <Iface> getObject(objectId: Int, interfaceClass: Class<Iface>): Iface {
|
||||||
// try {
|
return rmiSupport.getGlobalRemoteObject(getConnection(), this, objectId, interfaceClass)
|
||||||
// connection!!.getRemoteObject(objectId, callback)
|
}
|
||||||
// } catch (e: NullPointerException) {
|
|
||||||
// logger.error("Error getting remote object!", e)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the connection used by the client.
|
* 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
|
* This is preferred to [EndPoint.getConnections], as it properly does some error checking
|
||||||
*/
|
*/
|
||||||
// can =just use super.get connection?
|
fun getConnection(): C {
|
||||||
// override var connection: C = TODO()
|
return connection as C
|
||||||
// get() = field
|
}
|
||||||
// set(connection) {
|
|
||||||
// super.connection = connection
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Throws(ClientException::class)
|
@Throws(ClientException::class)
|
||||||
suspend fun send(message: Any) {
|
suspend fun send(message: Any) {
|
||||||
|
|
|
@ -16,14 +16,13 @@
|
||||||
package dorkbox.network
|
package dorkbox.network
|
||||||
|
|
||||||
import dorkbox.network.aeron.server.ServerException
|
import dorkbox.network.aeron.server.ServerException
|
||||||
import dorkbox.network.connection.Connection
|
import dorkbox.network.connection.*
|
||||||
import dorkbox.network.connection.ConnectionManagerServer
|
|
||||||
import dorkbox.network.connection.EndPoint
|
|
||||||
import dorkbox.network.connection.UdpMediaDriverConnection
|
|
||||||
import dorkbox.network.connection.connectionType.ConnectionProperties
|
import dorkbox.network.connection.connectionType.ConnectionProperties
|
||||||
import dorkbox.network.connection.connectionType.ConnectionRule
|
import dorkbox.network.connection.connectionType.ConnectionRule
|
||||||
import dorkbox.network.ipFilter.IpFilterRule
|
import dorkbox.network.ipFilter.IpFilterRule
|
||||||
import dorkbox.network.ipFilter.IpFilterRuleType
|
import dorkbox.network.ipFilter.IpFilterRuleType
|
||||||
|
import dorkbox.network.other.NetUtil
|
||||||
|
import dorkbox.network.other.NetworkUtil
|
||||||
import io.aeron.FragmentAssembler
|
import io.aeron.FragmentAssembler
|
||||||
import io.aeron.logbuffer.FragmentHandler
|
import io.aeron.logbuffer.FragmentHandler
|
||||||
import io.aeron.logbuffer.Header
|
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
|
// The is how clients then get the new ports to connect to + other configuration options
|
||||||
|
|
||||||
val handshakeDriver = UdpMediaDriverConnection(
|
val handshakeDriver = UdpMediaDriverConnection(
|
||||||
config.listenIpAddress, config.subscriptionPort, config.publicationPort,
|
address = config.listenIpAddress,
|
||||||
UDP_HANDSHAKE_STREAM_ID, RESERVED_SESSION_ID_INVALID)
|
subscriptionPort = config.subscriptionPort,
|
||||||
|
publicationPort = config.publicationPort,
|
||||||
|
streamId = UDP_HANDSHAKE_STREAM_ID,
|
||||||
|
sessionId = RESERVED_SESSION_ID_INVALID)
|
||||||
|
|
||||||
handshakeDriver.buildServer(aeron)
|
handshakeDriver.buildServer(aeron)
|
||||||
|
|
||||||
|
@ -155,7 +157,19 @@ open class Server<C : Connection>(config: ServerConfiguration = ServerConfigurat
|
||||||
val handshakeSubscription = handshakeDriver.subscription
|
val handshakeSubscription = handshakeDriver.subscription
|
||||||
|
|
||||||
logger.debug(handshakeDriver.serverInfo())
|
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:
|
* Note:
|
||||||
|
@ -171,17 +185,29 @@ open class Server<C : Connection>(config: ServerConfiguration = ServerConfigurat
|
||||||
connectionManager.receiveHandshakeMessageServer(handshakePublication, buffer, offset, length, header, this@Server)
|
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 {
|
actionDispatch.launch {
|
||||||
val pollIdleStrategy = config.pollIdleStrategy
|
val pollIdleStrategy = config.pollIdleStrategy
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
var pollCount: Int
|
||||||
while (!isShutdown()) {
|
while (!isShutdown()) {
|
||||||
|
pollCount = 0
|
||||||
|
|
||||||
// this checks to see if there are NEW clients
|
// 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)
|
// 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)
|
// 0 means we idle. >0 means reset and don't idle (because there are likely more poll events)
|
||||||
pollIdleStrategy.idle(pollCount)
|
pollIdleStrategy.idle(pollCount)
|
||||||
|
@ -189,6 +215,9 @@ open class Server<C : Connection>(config: ServerConfiguration = ServerConfigurat
|
||||||
} finally {
|
} finally {
|
||||||
handshakePublication.close()
|
handshakePublication.close()
|
||||||
handshakeSubscription.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
|
* If there is nothing added to this list - then ALL are permitted
|
||||||
*/
|
*/
|
||||||
fun addIpFilterRule(vararg rules: IpFilterRule?) {
|
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)
|
// 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.
|
* Adds a custom connection to the server.
|
||||||
*
|
*
|
||||||
|
|
|
@ -326,6 +326,13 @@ class CoroutineBackoffIdleStrategy : BackoffIdleStrategyData, CoroutineIdleStrat
|
||||||
return ALIAS
|
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 {
|
override fun toString(): String {
|
||||||
return "BackoffIdleStrategy{" +
|
return "BackoffIdleStrategy{" +
|
||||||
"alias=" + ALIAS +
|
"alias=" + ALIAS +
|
||||||
|
|
|
@ -102,4 +102,9 @@ interface CoroutineIdleStrategy {
|
||||||
fun alias(): String {
|
fun alias(): String {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a clone of this IdleStrategy
|
||||||
|
*/
|
||||||
|
fun clone(): CoroutineIdleStrategy
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,6 +91,14 @@ class CoroutineSleepingMillisIdleStrategy : CoroutineIdleStrategy {
|
||||||
return ALIAS
|
return ALIAS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a clone of this IdleStrategy
|
||||||
|
*/
|
||||||
|
override fun clone(): CoroutineSleepingMillisIdleStrategy {
|
||||||
|
return CoroutineSleepingMillisIdleStrategy(sleepPeriodMs = sleepPeriodMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "SleepingMillisIdleStrategy{" +
|
return "SleepingMillisIdleStrategy{" +
|
||||||
"alias=" + ALIAS +
|
"alias=" + ALIAS +
|
||||||
|
|
|
@ -2,11 +2,11 @@ package dorkbox.network.connection
|
||||||
|
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
|
|
||||||
data class ClientConnectionInfo(val subscriptionPort: Int,
|
class ClientConnectionInfo(val subscriptionPort: Int,
|
||||||
val publicationPort: Int,
|
val publicationPort: Int,
|
||||||
val sessionId: Int,
|
val sessionId: Int,
|
||||||
val streamId: Int,
|
val streamId: Int,
|
||||||
val publicKey: ByteArray) {
|
val publicKey: ByteArray) {
|
||||||
|
|
||||||
fun log(handshakeSessionId: Int, logger: Logger) {
|
fun log(handshakeSessionId: Int, logger: Logger) {
|
||||||
logger.debug("[{}] connect {} {} (encrypted {})", handshakeSessionId, subscriptionPort, publicationPort, sessionId)
|
logger.debug("[{}] connect {} {} (encrypted {})", handshakeSessionId, subscriptionPort, publicationPort, sessionId)
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package dorkbox.network.connection
|
package dorkbox.network.connection
|
||||||
|
|
||||||
import dorkbox.network.rmi.RemoteObjectCallback
|
|
||||||
|
|
||||||
interface Connection : AutoCloseable {
|
interface Connection : AutoCloseable {
|
||||||
/**
|
/**
|
||||||
* Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed.
|
* 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()
|
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"
|
* 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.
|
* to an object that is created remotely.
|
||||||
|
@ -158,7 +146,7 @@ interface Connection : AutoCloseable {
|
||||||
*
|
*
|
||||||
* @see RemoteObject
|
* @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"
|
* 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
|
* @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
|
package dorkbox.network.connection
|
||||||
|
|
||||||
import dorkbox.network.NetworkUtil
|
|
||||||
import dorkbox.network.Server
|
import dorkbox.network.Server
|
||||||
import dorkbox.network.connection.ping.PingFuture
|
import dorkbox.network.connection.ping.PingFuture
|
||||||
import dorkbox.network.connection.ping.PingMessage
|
import dorkbox.network.connection.ping.PingMessage
|
||||||
import dorkbox.network.rmi.ConnectionRmiSupport
|
import dorkbox.network.other.NetworkUtil
|
||||||
import dorkbox.network.rmi.RemoteObjectCallback
|
import dorkbox.network.rmi.RmiSupportConnection
|
||||||
|
import dorkbox.util.classes.ClassHelper
|
||||||
import io.aeron.FragmentAssembler
|
import io.aeron.FragmentAssembler
|
||||||
import io.aeron.Publication
|
import io.aeron.Publication
|
||||||
import io.aeron.Subscription
|
import io.aeron.Subscription
|
||||||
|
@ -33,7 +33,6 @@ import org.agrona.BitUtil
|
||||||
import org.agrona.BufferUtil
|
import org.agrona.BufferUtil
|
||||||
import org.agrona.DirectBuffer
|
import org.agrona.DirectBuffer
|
||||||
import org.agrona.concurrent.UnsafeBuffer
|
import org.agrona.concurrent.UnsafeBuffer
|
||||||
import org.slf4j.Logger
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -57,10 +56,13 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
|
||||||
final override val sessionId: Int
|
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 needsLock = AtomicBoolean(false)
|
||||||
// private val writeSignalNeeded = AtomicBoolean(false)
|
// private val writeSignalNeeded = AtomicBoolean(false)
|
||||||
|
@ -76,8 +78,8 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
|
||||||
private var pingFuture: PingFuture? = null
|
private var pingFuture: PingFuture? = null
|
||||||
|
|
||||||
// used to store connection local listeners (instead of global listeners). Only possible on the server.
|
// used to store connection local listeners (instead of global listeners). Only possible on the server.
|
||||||
@Volatile
|
// @Volatile
|
||||||
private var localListenerManager: ConnectionManager<*>? = null
|
// 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.
|
// while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error.
|
||||||
private var remoteKeyChanged = false
|
private var remoteKeyChanged = false
|
||||||
|
@ -91,7 +93,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
|
||||||
private var closeLatch: CountDownLatch? = null
|
private var closeLatch: CountDownLatch? = null
|
||||||
|
|
||||||
// RMI support for this connection
|
// RMI support for this connection
|
||||||
var rmiSupport: ConnectionRmiSupport
|
private val rmiSupportConnection: RmiSupportConnection<Connection_>
|
||||||
|
|
||||||
|
|
||||||
var messageHandler: FragmentAssembler
|
var messageHandler: FragmentAssembler
|
||||||
|
@ -119,7 +121,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
|
||||||
publicationPort = mediaDriverConnection.publicationPort
|
publicationPort = mediaDriverConnection.publicationPort
|
||||||
remoteAddress = mediaDriverConnection.address
|
remoteAddress = mediaDriverConnection.address
|
||||||
remoteAddressInt = NetworkUtil.IP.toInt(remoteAddress)
|
remoteAddressInt = NetworkUtil.IP.toInt(remoteAddress)
|
||||||
streamId = mediaDriverConnection.streamId
|
streamId = mediaDriverConnection.streamId // NOTE: this is UNIQUE per server!
|
||||||
sessionId = mediaDriverConnection.sessionId
|
sessionId = mediaDriverConnection.sessionId
|
||||||
|
|
||||||
messageHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
|
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?
|
// when closing this connection, HOW MANY endpoints need to be closed?
|
||||||
closeLatch = CountDownLatch(1)
|
closeLatch = CountDownLatch(1)
|
||||||
|
@ -139,8 +141,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
|
||||||
/**
|
/**
|
||||||
* @param now The current time
|
* @param now The current time
|
||||||
*
|
*
|
||||||
* @return `true` if this duologue has no subscribers and the current
|
* @return `true` if this connection has no subscribers and the current time `now` is after the expriation date
|
||||||
* time `now` is after the intended expiry date of the duologue
|
|
||||||
*/
|
*/
|
||||||
override fun isExpired(now: Long): Boolean {
|
override fun isExpired(now: Long): Boolean {
|
||||||
return subscription.imageCount() == 0 && now > expirationTime
|
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).
|
* Safely sends objects to a destination (such as a custom object or a standard ping).
|
||||||
*/
|
*/
|
||||||
override suspend fun send(message: Any) {
|
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
|
// remove all RMI listeners
|
||||||
rmiSupport.close()
|
// rmiSupport.close() // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// @Throws(Exception::class)
|
// @Throws(Exception::class)
|
||||||
// override fun exceptionCaught(context: ChannelHandlerContext, cause: Throwable) {
|
// override fun exceptionCaught(context: ChannelHandlerContext, cause: Throwable) {
|
||||||
// val channel = context.channel()
|
// val channel = context.channel()
|
||||||
|
@ -500,7 +537,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi
|
||||||
* This includes all proxy listeners
|
* This includes all proxy listeners
|
||||||
*/
|
*/
|
||||||
override fun removeAll(): Listeners<Connection> {
|
override fun removeAll(): Listeners<Connection> {
|
||||||
rmiSupport.removeAllListeners()
|
// rmiSupport.removeAllListeners() // TODO
|
||||||
|
|
||||||
if (endPoint is Server) {
|
if (endPoint is Server) {
|
||||||
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
// 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
|
// RMI methods
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
override fun rmiSupport(): ConnectionRmiSupport {
|
override fun rmiSupport(): RmiSupportConnection<Connection_> {
|
||||||
return rmiSupport
|
return rmiSupportConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>) {
|
override suspend fun <Iface> createObject(callback: suspend (Iface) -> Unit) {
|
||||||
rmiSupport.createRemoteObject(this, interfaceClass, callback)
|
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>) {
|
override fun <Iface> getObject(objectId: Int, interfaceClass: Class<Iface>): Iface {
|
||||||
rmiSupport.getRemoteObject(this, objectId, callback)
|
@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
|
private var failed = false
|
||||||
|
|
||||||
|
lateinit var handler: FragmentHandler
|
||||||
|
lateinit var endPoint: EndPoint<C>
|
||||||
var sessionId: Int = 0
|
var sessionId: Int = 0
|
||||||
|
|
||||||
@Throws(ClientTimedOutException::class, ClientRejectedException::class)
|
fun init(endPoint: EndPoint<C>) {
|
||||||
suspend fun initHandshake(mediaConnection: MediaDriverConnection, connectionTimeoutMS: Long, endPoint: EndPoint<C>) : ClientConnectionInfo {
|
this.endPoint = endPoint
|
||||||
// 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 ->
|
// 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 {
|
endPoint.actionDispatch.launch {
|
||||||
val message = endPoint.readHandshakeMessage(buffer, offset, length, header)
|
val message = endPoint.readHandshakeMessage(buffer, offset, length, header)
|
||||||
logger.debug("[{}] response: {}", sessionId, message)
|
logger.debug("[{}] response: {}", sessionId, message)
|
||||||
|
@ -54,19 +57,25 @@ class ConnectionManagerClient<C : Connection>(logger: Logger, config: Configurat
|
||||||
subscriptionPort = message.publicationPort,
|
subscriptionPort = message.publicationPort,
|
||||||
publicationPort = message.subscriptionPort,
|
publicationPort = message.subscriptionPort,
|
||||||
sessionId = oneTimePad xor message.oneTimePad,
|
sessionId = oneTimePad xor message.oneTimePad,
|
||||||
streamId = oneTimePad xor message.streamId,
|
streamId = oneTimePad xor message.streamId,
|
||||||
publicKey = message.publicKey!!)
|
publicKey = message.publicKey!!)
|
||||||
|
|
||||||
connectionInfo!!.log(sessionId, logger)
|
connectionInfo!!.log(sessionId, logger)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(ClientTimedOutException::class, ClientRejectedException::class)
|
||||||
val registrationMessage = Registration.hello(oneTimePad, config.settingsStore.getPublicKey()!!)
|
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.
|
// Send the one-time pad to the server.
|
||||||
endPoint.writeMessage(mediaConnection.publication, registrationMessage)
|
endPoint.writeHandshakeMessage(mediaConnection.publication, registrationMessage)
|
||||||
sessionId = mediaConnection.publication.sessionId()
|
sessionId = mediaConnection.publication.sessionId()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package dorkbox.network.connection
|
package dorkbox.network.connection
|
||||||
|
|
||||||
import dorkbox.network.NetworkUtil
|
|
||||||
import dorkbox.network.ServerConfiguration
|
import dorkbox.network.ServerConfiguration
|
||||||
import dorkbox.network.aeron.client.ClientRejectedException
|
import dorkbox.network.aeron.client.ClientRejectedException
|
||||||
import dorkbox.network.aeron.server.AllocationException
|
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.RandomIdAllocator
|
||||||
import dorkbox.network.aeron.server.ServerException
|
import dorkbox.network.aeron.server.ServerException
|
||||||
import dorkbox.network.connection.registration.Registration
|
import dorkbox.network.connection.registration.Registration
|
||||||
|
import dorkbox.network.other.NetworkUtil
|
||||||
import io.aeron.Image
|
import io.aeron.Image
|
||||||
import io.aeron.Publication
|
import io.aeron.Publication
|
||||||
import io.aeron.logbuffer.Header
|
import io.aeron.logbuffer.Header
|
||||||
|
@ -46,37 +46,36 @@ class ConnectionManagerServer<C : Connection>(logger: Logger,
|
||||||
suspend fun receiveHandshakeMessageServer(handshakePublication: Publication,
|
suspend fun receiveHandshakeMessageServer(handshakePublication: Publication,
|
||||||
buffer: DirectBuffer, offset: Int, length: Int, header: Header,
|
buffer: DirectBuffer, offset: Int, length: Int, header: Header,
|
||||||
endPoint: EndPoint<C>) {
|
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.
|
// 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()
|
val sessionId = header.sessionId()
|
||||||
|
|
||||||
// note: this address will ALWAYS be an IP:PORT combo
|
// note: this address will ALWAYS be an IP:PORT combo
|
||||||
val remoteIpAndPort = (header.context() as Image).sourceIdentity()
|
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 {
|
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.
|
// VALIDATE:: Check to see if there are already too many clients connected.
|
||||||
if (connectionCount() >= config.maxClientCount) {
|
if (connectionCount() >= config.maxClientCount) {
|
||||||
logger.debug("server is full")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,8 +86,12 @@ class ConnectionManagerServer<C : Connection>(logger: Logger,
|
||||||
return
|
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.
|
// VALIDATE:: we are now connected to the client and are going to create a new connection.
|
||||||
val currentCountForIp = connectionsPerIpCounts.getAndIncrement(clientAddress)
|
val currentCountForIp = connectionsPerIpCounts.getAndIncrement(clientAddress)
|
||||||
|
@ -97,109 +100,82 @@ class ConnectionManagerServer<C : Connection>(logger: Logger,
|
||||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
||||||
|
|
||||||
logger.debug("too many connections for IP address")
|
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
|
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
|
||||||
|
/////
|
||||||
|
/////
|
||||||
|
|
||||||
|
|
||||||
/////
|
// allocate ports for the client
|
||||||
/////
|
val connectionPorts: IntArray
|
||||||
///// DONE WITH VALIDATION
|
|
||||||
/////
|
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 connectionStreamId: Int
|
||||||
val connectionPorts: IntArray
|
try {
|
||||||
|
connectionStreamId = streamIdAllocator.allocate()
|
||||||
|
} catch (e: AllocationException) {
|
||||||
|
// have to unwind actions!
|
||||||
|
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
||||||
|
portAllocator.free(connectionPorts)
|
||||||
|
sessionIdAllocator.free(connectionSessionId)
|
||||||
|
|
||||||
try {
|
logger.error("Unable to allocate a stream ID for the client connection!")
|
||||||
// throws exception if this is not possible
|
return
|
||||||
connectionPorts = portAllocator.allocate(portsPerClient)
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
// have to unwind actions!
|
|
||||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
|
||||||
|
|
||||||
logger.error("Unable to allocate $portsPerClient ports for client connection!")
|
val serverAddress = config.listenIpAddress // TODO :: my IP address?? this should be the IP of the box?
|
||||||
return
|
val subscriptionPort = connectionPorts[0]
|
||||||
}
|
val publicationPort = connectionPorts[1]
|
||||||
|
|
||||||
// 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 connectionStreamId: Int
|
// create a new connection. The session ID is encrypted.
|
||||||
try {
|
try {
|
||||||
connectionStreamId = streamIdAllocator.allocate()
|
// connection timeout of 0 doesn't matter. it is not used by the server
|
||||||
} catch (e: AllocationException) {
|
val clientConnection = UdpMediaDriverConnection(
|
||||||
// have to unwind actions!
|
serverAddress, subscriptionPort, publicationPort,
|
||||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
connectionStreamId, connectionSessionId, 0, message.isReliable)
|
||||||
portAllocator.free(connectionPorts)
|
|
||||||
sessionIdAllocator.free(connectionSessionId)
|
|
||||||
|
|
||||||
logger.error("Unable to allocate a stream ID for the client connection!")
|
val connection: Connection = endPoint.newConnection(endPoint, clientConnection)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val serverAddress = config.listenIpAddress // TODO :: my IP address?? this should be the IP of the box?
|
// VALIDATE:: are we allowed to connect to this server (now that we have the initial server information)
|
||||||
val subscriptionPort = connectionPorts[0]
|
@Suppress("UNCHECKED_CAST")
|
||||||
val publicationPort = connectionPorts[1]
|
val permitConnection = notifyFilter(connection as C)
|
||||||
|
if (!permitConnection) {
|
||||||
|
|
||||||
// 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) {
|
|
||||||
// have to unwind actions!
|
// have to unwind actions!
|
||||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
||||||
portAllocator.free(connectionPorts)
|
portAllocator.free(connectionPorts)
|
||||||
|
@ -208,11 +184,37 @@ class ConnectionManagerServer<C : Connection>(logger: Logger,
|
||||||
|
|
||||||
logger.error("Error creating new duologue")
|
logger.error("Error creating new duologue")
|
||||||
|
|
||||||
logger.error("could not process client message: $message")
|
notifyError(connection, ClientRejectedException("Connection was not permitted!"))
|
||||||
notifyError(e)
|
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) {
|
} 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
|
package dorkbox.network.connection
|
||||||
|
|
||||||
import dorkbox.network.rmi.ConnectionRmiSupport
|
import dorkbox.network.rmi.RmiSupportConnection
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,7 +25,7 @@ interface Connection_ : Connection {
|
||||||
/**
|
/**
|
||||||
* @return the RMI support for this connection
|
* @return the RMI support for this connection
|
||||||
*/
|
*/
|
||||||
fun rmiSupport(): ConnectionRmiSupport
|
fun rmiSupport(): RmiSupportConnection<Connection_>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the per-message sequence number.
|
* This is the per-message sequence number.
|
||||||
|
|
|
@ -15,14 +15,17 @@
|
||||||
*/
|
*/
|
||||||
package dorkbox.network.connection
|
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.aeron.CoroutineIdleStrategy
|
||||||
import dorkbox.network.connection.ping.PingMessage
|
import dorkbox.network.connection.ping.PingMessage
|
||||||
import dorkbox.network.other.CryptoEccNative
|
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.rmi.messages.RmiMessage
|
||||||
import dorkbox.network.serialization.NetworkSerializationManager
|
import dorkbox.network.serialization.NetworkSerializationManager
|
||||||
import dorkbox.network.serialization.Serialization
|
|
||||||
import dorkbox.network.store.NullSettingsStore
|
import dorkbox.network.store.NullSettingsStore
|
||||||
import dorkbox.network.store.SettingsStore
|
import dorkbox.network.store.SettingsStore
|
||||||
import dorkbox.util.NamedThreadFactory
|
import dorkbox.util.NamedThreadFactory
|
||||||
|
@ -38,9 +41,9 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import mu.KLogger
|
||||||
|
import mu.KotlinLogging
|
||||||
import org.agrona.DirectBuffer
|
import org.agrona.DirectBuffer
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.KeyFactory
|
import java.security.KeyFactory
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
|
@ -58,7 +61,12 @@ import java.util.concurrent.CountDownLatch
|
||||||
// Usually it's with ISPs.
|
// Usually it's with ISPs.
|
||||||
/**
|
/**
|
||||||
* represents the base of a client/server end point for interacting with aeron
|
* 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>
|
abstract class EndPoint<C : Connection>
|
||||||
internal constructor(val type: Class<*>, internal val config: Configuration) : AutoCloseable {
|
internal constructor(val type: Class<*>, internal val config: Configuration) : AutoCloseable {
|
||||||
protected constructor(config: Configuration) : this(Client::class.java, config)
|
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 RESERVED_SESSION_ID_HIGH = Integer.MAX_VALUE
|
||||||
|
|
||||||
const val UDP_HANDSHAKE_STREAM_ID: Int = 0x1337cafe
|
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 {
|
init {
|
||||||
println("THIS IS ONLY IPV4 AT THE MOMENT. IPV6 is in progress!")
|
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 logger: KLogger = KotlinLogging.logger(type.simpleName)
|
||||||
val name = type.simpleName
|
|
||||||
val logger: Logger = LoggerFactory.getLogger(name)
|
|
||||||
|
|
||||||
internal val closables = CopyOnWriteArrayList<AutoCloseable>()
|
internal val closables = CopyOnWriteArrayList<AutoCloseable>()
|
||||||
|
|
||||||
internal val actionDispatch = CoroutineScope(Dispatchers.Default)
|
internal val actionDispatch = CoroutineScope(Dispatchers.Default)
|
||||||
internal abstract val connectionManager: ConnectionManager<C>
|
internal abstract val connectionManager: ConnectionManager<C>
|
||||||
|
|
||||||
internal lateinit var mediaDriver: MediaDriver
|
internal val mediaDriverContext: MediaDriver.Context
|
||||||
internal lateinit var aeron: Aeron
|
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.
|
* 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
|
// we only want one instance of these created. These will be called appropriately
|
||||||
val settingsStore: SettingsStore
|
val settingsStore: SettingsStore
|
||||||
val rmiGlobalBridge = RmiServer(logger, true)
|
|
||||||
var disableRemoteKeyValidation = false
|
var disableRemoteKeyValidation = false
|
||||||
|
|
||||||
|
val rmiSupport = RmiSupport<C>(logger, actionDispatch, config.serialization)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks to see if this client has connected yet or not.
|
* 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
|
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 {
|
init {
|
||||||
// Aeron configuration
|
// Aeron configuration
|
||||||
|
|
||||||
|
@ -173,8 +176,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
|
||||||
// OS.isMacOsX() ->
|
// OS.isMacOsX() ->
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// val rmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.rmem_max")
|
// val rmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.rmem_max")
|
||||||
// val wmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.wmem_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() ->
|
// OS.isMacOsX() ->
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// val rmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.rmem_max")
|
// val rmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.rmem_max")
|
||||||
// val wmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.wmem_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)
|
val aeronLogDirectory = File(baseFile, baseName)
|
||||||
if (aeronLogDirectory.exists()) {
|
if (aeronLogDirectory.exists()) {
|
||||||
logger.error("Aeron log directory already exists! This might not be what you want!")
|
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")
|
logger.debug("Aeron log directory: $aeronLogDirectory")
|
||||||
config.aeronLogDirectory = aeronLogDirectory
|
config.aeronLogDirectory = aeronLogDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
// the RmiNoOpConnection must have an endpoint, and we DO NOT want it to actually setup/configure aeron!
|
val threadFactory = NamedThreadFactory("Aeron", false)
|
||||||
if (config.publicationPort > 0) {
|
|
||||||
val threadFactory = NamedThreadFactory("Aeron", false)
|
|
||||||
|
|
||||||
// LOW-LATENCY SETTINGS
|
// LOW-LATENCY SETTINGS
|
||||||
// .termBufferSparseFile(false)
|
// .termBufferSparseFile(false)
|
||||||
// .useWindowsHighResTimer(true)
|
// .useWindowsHighResTimer(true)
|
||||||
// .threadingMode(ThreadingMode.DEDICATED)
|
// .threadingMode(ThreadingMode.DEDICATED)
|
||||||
// .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE)
|
// .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE)
|
||||||
// .receiverIdleStrategy(NoOpIdleStrategy.INSTANCE)
|
// .receiverIdleStrategy(NoOpIdleStrategy.INSTANCE)
|
||||||
// .senderIdleStrategy(NoOpIdleStrategy.INSTANCE);
|
// .senderIdleStrategy(NoOpIdleStrategy.INSTANCE);
|
||||||
val mediaDriverContext = MediaDriver.Context()
|
mediaDriverContext = MediaDriver.Context()
|
||||||
.publicationReservedSessionIdLow(RESERVED_SESSION_ID_LOW)
|
.publicationReservedSessionIdLow(RESERVED_SESSION_ID_LOW)
|
||||||
.publicationReservedSessionIdHigh(RESERVED_SESSION_ID_HIGH)
|
.publicationReservedSessionIdHigh(RESERVED_SESSION_ID_HIGH)
|
||||||
.dirDeleteOnStart(true) // TODO: FOR NOW?
|
.dirDeleteOnStart(true) // TODO: FOR NOW?
|
||||||
.dirDeleteOnShutdown(true)
|
.dirDeleteOnShutdown(true)
|
||||||
.conductorThreadFactory(threadFactory)
|
.conductorThreadFactory(threadFactory)
|
||||||
.receiverThreadFactory(threadFactory)
|
.receiverThreadFactory(threadFactory)
|
||||||
.senderThreadFactory(threadFactory)
|
.senderThreadFactory(threadFactory)
|
||||||
.sharedNetworkThreadFactory(threadFactory)
|
.sharedNetworkThreadFactory(threadFactory)
|
||||||
.sharedThreadFactory(threadFactory)
|
.sharedThreadFactory(threadFactory)
|
||||||
.threadingMode(config.threadingMode)
|
.threadingMode(config.threadingMode)
|
||||||
.mtuLength(config.networkMtuSize)
|
.mtuLength(config.networkMtuSize)
|
||||||
.socketSndbufLength(config.sendBufferSize)
|
.socketSndbufLength(config.sendBufferSize)
|
||||||
.socketRcvbufLength(config.receiveBufferSize)
|
.socketRcvbufLength(config.receiveBufferSize)
|
||||||
.aeronDirectoryName(config.aeronLogDirectory!!.absolutePath)
|
.aeronDirectoryName(config.aeronLogDirectory!!.absolutePath)
|
||||||
|
|
||||||
val aeronContext = Aeron.Context().aeronDirectoryName(mediaDriverContext.aeronDirectoryName())
|
val aeronContext = Aeron.Context().aeronDirectoryName(mediaDriverContext.aeronDirectoryName())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mediaDriver = MediaDriver.launch(mediaDriverContext)
|
mediaDriver = MediaDriver.launch(mediaDriverContext)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw e
|
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 {
|
||||||
|
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 stuff
|
||||||
serialization = config.serialization
|
serialization = config.serialization
|
||||||
|
@ -417,6 +415,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
|
||||||
*/
|
*/
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
open fun newConnection(endPoint: EndPoint<C>, mediaDriverConnection: MediaDriverConnection): C {
|
open fun newConnection(endPoint: EndPoint<C>, mediaDriverConnection: MediaDriverConnection): C {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
return ConnectionImpl(endPoint, mediaDriverConnection) as C
|
return ConnectionImpl(endPoint, mediaDriverConnection) as C
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,56 +497,13 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
|
||||||
connectionManager.forEachConnectionDoRead(function)
|
connectionManager.forEachConnectionDoRead(function)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
internal suspend fun writeHandshakeMessage(publication: Publication, message: Any) {
|
||||||
* Creates a "global" RMI object for use by multiple connections.
|
// The sessionId is globally unique, and is assigned by the server.
|
||||||
*
|
|
||||||
* @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()!!
|
|
||||||
logger.debug("[{}] send: {}", publication.sessionId(), message)
|
logger.debug("[{}] send: {}", publication.sessionId(), message)
|
||||||
|
|
||||||
val kryo: KryoExtra = serialization.takeKryo()
|
val kryo: KryoExtra = serialization.takeKryo()
|
||||||
try {
|
try {
|
||||||
kryo.write(connection, message)
|
kryo.write(message)
|
||||||
|
|
||||||
val buffer = kryo.writerBuffer
|
val buffer = kryo.writerBuffer
|
||||||
val objectSize = buffer.position()
|
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.
|
// more critical error sending the message. we shouldn't retry or anything.
|
||||||
logger.error("Error sending message. ${errorCodeName(result)}")
|
logger.error("Error sending message. ${errorCodeName(result)}")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error("Error serializing message $message", e)
|
logger.error("Error serializing message $message", e)
|
||||||
} finally {
|
} finally {
|
||||||
|
sendIdleStrategy.reset()
|
||||||
serialization.returnKryo(kryo)
|
serialization.returnKryo(kryo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param buffer The buffer
|
* @param buffer The buffer
|
||||||
* @param offset The offset from the start of 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
|
* @return A string
|
||||||
*/
|
*/
|
||||||
fun readHandshakeMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header): Any? {
|
internal 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()!!
|
|
||||||
|
|
||||||
val kryo: KryoExtra = serialization.takeKryo()
|
val kryo: KryoExtra = serialization.takeKryo()
|
||||||
try {
|
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) {
|
} catch (e: Exception) {
|
||||||
logger.error("Error de-serializing message on connection ${header.sessionId()}!", e)
|
logger.error("Error de-serializing message on connection ${header.sessionId()}!", e)
|
||||||
} finally {
|
} 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_) {
|
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()
|
val sessionId = header.sessionId()
|
||||||
|
|
||||||
// note: this address will ALWAYS be an IP:PORT combo
|
// 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)
|
// val ipAsInt = NetworkUtil.IP.toInt(ip)
|
||||||
|
|
||||||
|
|
||||||
// TODO: WE MIGHT NOT WANT TO USE SESSIONID()!!
|
|
||||||
var message: Any? = null
|
var message: Any? = null
|
||||||
|
|
||||||
val kryo: KryoExtra = serialization.takeKryo()
|
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)
|
message = kryo.read(buffer, offset, length, connection)
|
||||||
logger.debug("[{}] received: {}", sessionId, message)
|
logger.debug("[{}] received: {}", sessionId, message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error("[${header.sessionId()}] Error de-serializing message", e)
|
logger.error("[${sessionId}] Error de-serializing message", e)
|
||||||
} finally {
|
} finally {
|
||||||
serialization.returnKryo(kryo)
|
serialization.returnKryo(kryo)
|
||||||
}
|
}
|
||||||
|
@ -647,7 +589,6 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
|
||||||
val data = ByteArray(length)
|
val data = ByteArray(length)
|
||||||
buffer.getBytes(offset, data)
|
buffer.getBytes(offset, data)
|
||||||
|
|
||||||
|
|
||||||
when (message) {
|
when (message) {
|
||||||
is PingMessage -> {
|
is PingMessage -> {
|
||||||
// the ping listener (internal use only!)
|
// the ping listener (internal use only!)
|
||||||
|
@ -664,8 +605,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
|
||||||
is RmiMessage -> {
|
is RmiMessage -> {
|
||||||
// if we are an RMI message/registration, we have very specific, defined behavior.
|
// 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
|
// We do not use the "normal" listener callback pattern because this require special functionality
|
||||||
// note: RMI messages are NEVER subclassed!
|
@Suppress("UNCHECKED_CAST")
|
||||||
connection.rmiSupport().manage(connection, message, logger)
|
rmiSupport.manage(this as EndPoint<Connection_>, connection, message, logger)
|
||||||
}
|
}
|
||||||
is Any -> {
|
is Any -> {
|
||||||
connectionManager.notifyOnMessage(connection, message)
|
connectionManager.notifyOnMessage(connection, message)
|
||||||
|
@ -697,7 +638,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
|
||||||
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "EndPoint [$name]"
|
return "EndPoint [${type.simpleName}]"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
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 com.esotericsoftware.kryo.io.Output
|
||||||
import dorkbox.network.pipeline.AeronInput
|
import dorkbox.network.pipeline.AeronInput
|
||||||
import dorkbox.network.pipeline.AeronOutput
|
import dorkbox.network.pipeline.AeronOutput
|
||||||
import dorkbox.network.rmi.ConnectionRmiSupport
|
import dorkbox.network.rmi.CachedMethod
|
||||||
import dorkbox.network.serialization.NetworkSerializationManager
|
|
||||||
import dorkbox.util.OS
|
import dorkbox.util.OS
|
||||||
import dorkbox.util.bytes.OptimizeUtilsByteArray
|
import dorkbox.util.bytes.OptimizeUtilsByteArray
|
||||||
import dorkbox.util.bytes.OptimizeUtilsByteBuf
|
import dorkbox.util.bytes.OptimizeUtilsByteBuf
|
||||||
|
@ -29,15 +28,15 @@ import io.netty.buffer.ByteBuf
|
||||||
import io.netty.buffer.ByteBufUtil
|
import io.netty.buffer.ByteBufUtil
|
||||||
import net.jpountz.lz4.LZ4Factory
|
import net.jpountz.lz4.LZ4Factory
|
||||||
import org.agrona.DirectBuffer
|
import org.agrona.DirectBuffer
|
||||||
|
import org.agrona.collections.Int2ObjectHashMap
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nothing in this class is thread safe
|
* 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
|
// for kryo serialization
|
||||||
private val readerBuffer = AeronInput()
|
private val readerBuffer = AeronInput()
|
||||||
val writerBuffer = AeronOutput()
|
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
|
// volatile to provide object visibility for entire class. This is unique per connection
|
||||||
lateinit var rmiSupport: ConnectionRmiSupport
|
|
||||||
lateinit var connection: Connection_
|
lateinit var connection: Connection_
|
||||||
|
|
||||||
private val secureRandom = SecureRandom()
|
// private val secureRandom = SecureRandom()
|
||||||
private var cipher: Cipher? = null
|
private var cipher: Cipher? = null
|
||||||
private val compressor = factory.fastCompressor()
|
private val compressor = factory.fastCompressor()
|
||||||
private val decompressor = factory.fastDecompressor()
|
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:
|
* OUTPUT:
|
||||||
* ++++++++++++++++++++++++++
|
* ++++++++++++++++++++++++++
|
||||||
|
@ -91,12 +107,27 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
|
||||||
fun write(connection: Connection_, message: Any) {
|
fun write(connection: Connection_, message: Any) {
|
||||||
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||||
this.connection = connection
|
this.connection = connection
|
||||||
this.rmiSupport = connection.rmiSupport()
|
|
||||||
|
|
||||||
writerBuffer.reset()
|
writerBuffer.reset()
|
||||||
writeClassAndObject(writerBuffer, message)
|
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:
|
* INPUT:
|
||||||
* ++++++++++++++++++++++++++
|
* ++++++++++++++++++++++++++
|
||||||
|
@ -107,7 +138,6 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
|
||||||
fun read(buffer: DirectBuffer, offset: Int, length: Int, connection: Connection_): Any {
|
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
|
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||||
this.connection = connection
|
this.connection = connection
|
||||||
this.rmiSupport = connection.rmiSupport()
|
|
||||||
|
|
||||||
// this properly sets the buffer info
|
// this properly sets the buffer info
|
||||||
readerBuffer.setBuffer(buffer, offset, length)
|
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:
|
* OUTPUT:
|
||||||
* ++++++++++++++++++++++++++
|
* ++++++++++++++++++++++++++
|
||||||
|
@ -133,13 +177,24 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
|
||||||
private fun write(connection: Connection_, writer: Output, message: Any) {
|
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
|
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||||
this.connection = connection
|
this.connection = connection
|
||||||
this.rmiSupport = connection.rmiSupport()
|
|
||||||
|
|
||||||
// write the object to the NORMAL output buffer!
|
// write the object to the NORMAL output buffer!
|
||||||
writer.reset()
|
writer.reset()
|
||||||
writeClassAndObject(writer, message)
|
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:
|
* INPUT:
|
||||||
* ++++++++++++++++++++++++++
|
* ++++++++++++++++++++++++++
|
||||||
|
@ -149,11 +204,54 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
|
||||||
private fun read(connection: Connection_, reader: Input): Any {
|
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
|
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||||
this.connection = connection
|
this.connection = connection
|
||||||
this.rmiSupport = connection.rmiSupport()
|
|
||||||
|
|
||||||
return readClassAndObject(reader)
|
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:
|
* BUFFER:
|
||||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
@ -196,6 +294,60 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo()
|
||||||
buffer.writeBytes(compressOutput, 0, compressedLength)
|
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:
|
* BUFFER:
|
||||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
|
@ -17,22 +17,6 @@ package dorkbox.network.connection
|
||||||
|
|
||||||
interface Listener {}
|
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.
|
* 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)
|
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.
|
* accidentally add an incompatible connection type.
|
||||||
*/
|
*/
|
||||||
interface Listeners<C> where C : Connection {
|
interface Listeners<C> where C : Connection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a function that will be called BEFORE a client/server "connects" with
|
* 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
|
* 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
|
* 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,
|
class UdpMediaDriverConnection(override val address: String,
|
||||||
override val subscriptionPort: Int,
|
override val subscriptionPort: Int,
|
||||||
override val publicationPort: Int,
|
override val publicationPort: Int,
|
||||||
override val streamId: Int,
|
override val streamId: Int,
|
||||||
override val sessionId: Int,
|
override val sessionId: Int,
|
||||||
private val connectionTimeoutMS: Long = 0,
|
private val connectionTimeoutMS: Long = 0,
|
||||||
override val isReliable: Boolean = true) : MediaDriverConnection {
|
override val isReliable: Boolean = true) : MediaDriverConnection {
|
||||||
|
|
||||||
override lateinit var subscription: Subscription
|
override lateinit var subscription: Subscription
|
||||||
override lateinit var publication: Publication
|
override lateinit var publication: Publication
|
||||||
|
@ -174,3 +174,137 @@ class UdpMediaDriverConnection(override val address: String,
|
||||||
return "$address [$subscriptionPort|$publicationPort] [$streamId|$sessionId] (reliable:$isReliable)"
|
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
|
// -1 means there is an error
|
||||||
var state = INVALID
|
var state = INVALID
|
||||||
|
|
||||||
var errorMessage: String? = null
|
var errorMessage: String? = null
|
||||||
var publicationPort = 0
|
var publicationPort = 0
|
||||||
var subscriptionPort = 0
|
var subscriptionPort = 0
|
||||||
var sessionId = 0
|
var sessionId = 0
|
||||||
var streamId = 0
|
var streamId = 0
|
||||||
|
|
||||||
var publicKey: ByteArray? = null
|
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
|
// 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
|
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!
|
// NOTE: this is for ECDSA!
|
||||||
// var eccParameters: IESParameters? = null
|
// 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
|
// > 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)
|
// 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 = 0
|
||||||
const val HELLO_ACK = 1
|
const val HELLO_ACK = 1
|
||||||
|
|
||||||
fun hello(oneTimePad: Int, publicKey: ByteArray): Registration {
|
fun hello(oneTimePad: Int, publicKey: ByteArray, registrationData: ByteArray): Registration {
|
||||||
val hello = Registration()
|
val hello = Registration()
|
||||||
hello.state = HELLO
|
hello.state = HELLO
|
||||||
hello.oneTimePad = oneTimePad
|
hello.oneTimePad = oneTimePad
|
||||||
hello.publicKey = publicKey
|
hello.publicKey = publicKey
|
||||||
|
hello.registrationData = registrationData
|
||||||
return hello
|
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
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.network;
|
package dorkbox.network.other;
|
||||||
|
|
||||||
import static io.netty.util.AsciiString.indexOf;
|
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.BufferedWriter;
|
||||||
import java.io.File;
|
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
|
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<*>?>) {
|
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
|
var responseTimeout: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the blocking behavior when invoking a remote method. Default is false (blocking).
|
* @return the ID of response for the last method invocation.
|
||||||
*
|
|
||||||
* @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.
|
|
||||||
*/
|
*/
|
||||||
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.
|
* Permits calls to [Object.toString] to actually return the `toString()` method on the object.
|
||||||
|
@ -69,26 +73,37 @@ interface RemoteObject {
|
||||||
*/
|
*/
|
||||||
fun enableToString(enableDetailedToString: Boolean)
|
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.
|
* 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
|
* @return the response of the last method invocation
|
||||||
*/
|
*/
|
||||||
fun waitForLastResponse(): Any?
|
suspend fun waitForLastResponse(): Any?
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the ID of response for the last method invocation.
|
|
||||||
*/
|
|
||||||
val lastResponseID: Byte
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for the specified method invocation response to be received or the response timeout to be reached.
|
* 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
|
* @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.
|
* 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
|
* Callback for creating remote RMI classes
|
||||||
*/
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
interface RemoteObjectCallback<Iface> {
|
interface RemoteObjectCallback<Iface> {
|
||||||
/**
|
/**
|
||||||
* @param remoteObject the remote object (as a proxy object) or null if there was an error creating the RMI object
|
* @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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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
|
package dorkbox.network.rmi
|
||||||
|
|
||||||
import com.conversantmedia.util.concurrent.MultithreadConcurrentQueue
|
|
||||||
import dorkbox.network.connection.Connection
|
import dorkbox.network.connection.Connection
|
||||||
import dorkbox.network.connection.Connection_
|
import dorkbox.network.other.SuspendWaiter
|
||||||
import dorkbox.network.connection.KryoExtra
|
import dorkbox.network.other.invokeSuspendFunction
|
||||||
import dorkbox.network.connection.OnMessageReceived
|
|
||||||
import dorkbox.network.rmi.messages.MethodRequest
|
import dorkbox.network.rmi.messages.MethodRequest
|
||||||
import dorkbox.network.rmi.messages.MethodResponse
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.agrona.collections.IntArrayList
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.*
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
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 connection this is really the network client -- there is ONLY ever 1 connection
|
||||||
* @param rmiSupport is used to provide RMI support
|
* @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 rmiObjectId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID
|
||||||
* @param iFace this is the RMI interface
|
* @param cachedMethods this is the methods available for the specified class
|
||||||
*/
|
*/
|
||||||
class RmiClient(private val connection: Connection_,
|
class RmiClient(val isGlobal: Boolean,
|
||||||
private val rmiSupport: ConnectionRmiSupport, // this is the RMI id
|
|
||||||
val rmiObjectId: Int,
|
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 {
|
companion object {
|
||||||
private val methods = RmiUtils.getMethods(RemoteObject::class.java)
|
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 setResponseTimeoutMethod = methods.find { it.name == "setResponseTimeout" }
|
||||||
private val getResponseTimeoutMethod = methods.find { it.name == "getResponseTimeout" }
|
private val getResponseTimeoutMethod = methods.find { it.name == "getResponseTimeout" }
|
||||||
private val setAsyncMethod = methods.find { it.name == "setAsync" }
|
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 enableToStringMethod = methods.find { it.name == "enableToString" }
|
||||||
|
private val enableWaitingForResponseMethod = methods.find { it.name == "enableWaitingForResponse" }
|
||||||
private val waitForLastResponseMethod = methods.find { it.name == "waitForLastResponse" }
|
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 waitForResponseMethod = methods.find { it.name == "waitForResponse" }
|
||||||
private val toStringMethod = methods.find { it.name == "toString" }
|
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 var timeoutMillis: Long = 3000
|
||||||
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 isAsync = false
|
private var isAsync = false
|
||||||
|
private var allowWaiting = false
|
||||||
private var enableToString = false
|
private var enableToString = false
|
||||||
|
|
||||||
|
// this is really a a short!
|
||||||
// for responseId's, "0" means no response (or invalid response id)
|
@Volatile
|
||||||
private val responseIds = MultithreadConcurrentQueue<Byte>(256)
|
private var previousResponseId: Int = 0
|
||||||
|
|
||||||
private var previousResponseId: Byte = 0x00
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
private suspend fun invokeSuspend(method: Method, args: Array<Any>): Any? {
|
||||||
// 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
|
|
||||||
|
|
||||||
// there is a STRANGE problem, where if we DO NOT respond/reply to method invocation, and immediate invoke multiple methods --
|
// 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
|
// the "server" side can have out-of-order method invocation. There are 2 ways to solve this
|
||||||
// 1) make the "server" side single threaded
|
// 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 (even if it is a void response). This simplifies our response mask, and lets us use more bits for storing the
|
||||||
// response ID
|
// response ID
|
||||||
|
|
||||||
|
val responseStorage = rmiSupportCache.getResponseStorage()
|
||||||
|
|
||||||
// If we are async, we always ignore the response.
|
// 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 value of 0 means to not respond, and the rest is just an ID to match requests <-> responses
|
// a thing (so we will know when to stop blocking).
|
||||||
if (isAsync) {
|
val responseId = responseStorage.prep(rmiObjectId, responseWaiter)
|
||||||
responseId = 0x00
|
|
||||||
responseIdAsInt = 0
|
|
||||||
} else {
|
|
||||||
responseId = responseIds.poll()
|
|
||||||
responseIdAsInt = 0xFF and responseId.toInt()
|
|
||||||
|
|
||||||
synchronized(this) {
|
// so we can query for async, if we want to necessary
|
||||||
pendingResponses[responseIdAsInt] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
invokeMethod.responseId = responseId
|
|
||||||
}
|
|
||||||
|
|
||||||
// so we can query this, if necessary
|
|
||||||
previousResponseId = responseId
|
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
|
// which method do we access? We always want to access the IMPLEMENTATION (if available!). we know that this will always succeed
|
||||||
val endPoint = connection.endPoint()
|
// this should be accessed via the KRYO class ID + method index (both are SHORT, and can be packed)
|
||||||
endPoint.actionDispatch.launch {
|
invokeMethod.cachedMethod = cachedMethods.first { it.method == method }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
connection.send(invokeMethod)
|
||||||
|
|
||||||
|
// if we are async, then this will immediately return!
|
||||||
return try {
|
return try {
|
||||||
val result = waitForResponse(responseIdAsInt)
|
val result = responseStorage.waitForReply(allowWaiting, isAsync, rmiObjectId, responseId, responseWaiter, timeoutMillis)
|
||||||
if (result is Exception) {
|
if (result is Exception) {
|
||||||
throw result
|
throw result
|
||||||
} else {
|
} else {
|
||||||
|
@ -324,58 +117,146 @@ class RmiClient(private val connection: Connection_,
|
||||||
}
|
}
|
||||||
} catch (ex: TimeoutException) {
|
} catch (ex: TimeoutException) {
|
||||||
throw TimeoutException("Response timed out: ${method.declaringClass.name}.${method.name}")
|
throw TimeoutException("Response timed out: ${method.declaringClass.name}.${method.name}")
|
||||||
} finally {
|
|
||||||
synchronized(this) {
|
|
||||||
pendingResponses[responseIdAsInt] = false
|
|
||||||
responseTable[responseIdAsInt] = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Suppress("DuplicatedCode")
|
||||||
* A timeout of 0 means that we want to disable waiting, otherwise - it waits in milliseconds
|
@Throws(Exception::class)
|
||||||
*/
|
override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
|
||||||
@Throws(IOException::class)
|
if (method.declaringClass == RemoteObject::class.java) {
|
||||||
private fun waitForResponse(responseIdAsInt: Int): Any? {
|
// manage all of the RemoteObject proxy methods
|
||||||
// if timeout == 0, we wait "forever"
|
when (method) {
|
||||||
var remaining: Long
|
closeMethod -> {
|
||||||
val endTime: Long
|
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) {
|
return previousResponseId
|
||||||
remaining = timeoutMillis.toLong()
|
}
|
||||||
endTime = System.currentTimeMillis() + remaining
|
enableWaitingForResponseMethod -> {
|
||||||
} else {
|
allowWaiting = args!![0] as Boolean
|
||||||
// not forever, but close enough
|
return null
|
||||||
remaining = Long.MAX_VALUE
|
}
|
||||||
endTime = Long.MAX_VALUE
|
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
|
// if a 'suspend' function is called, then our last argument is a 'Continuation' object
|
||||||
var methodResponse: MethodResponse?
|
// We will use this for our coroutine context instead of running on a new coroutine
|
||||||
while (remaining > 0) {
|
val maybeContinuation = args?.lastOrNull()
|
||||||
synchronized(this) {
|
|
||||||
methodResponse = responseTable[responseIdAsInt]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (isAsync) {
|
||||||
if (methodResponse != null) {
|
// return immediately, without suspends
|
||||||
previousResponseId = 0 // 0 is "no response" or "invalid"
|
if (maybeContinuation is Continuation<*>) {
|
||||||
return methodResponse!!.result
|
val argsWithoutContinuation = args.take(args.size - 1)
|
||||||
|
invokeSuspendFunction(maybeContinuation) {
|
||||||
|
invokeSuspend(method, argsWithoutContinuation.toTypedArray())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lock.lock()
|
runBlocking {
|
||||||
try {
|
invokeSuspend(method, args ?: EMPTY_ARRAY)
|
||||||
responseCondition.await(remaining, TimeUnit.MILLISECONDS)
|
}
|
||||||
} catch (e: InterruptedException) {
|
}
|
||||||
Thread.currentThread()
|
|
||||||
.interrupt()
|
// if we are async then we return immediately. If you want the response value, you MUST use
|
||||||
throw IOException("Response timed out.", e)
|
// 'waitForLastResponse()' or 'waitForResponse'('getLastResponseID()')
|
||||||
} finally {
|
val returnType = method.returnType
|
||||||
lock.unlock()
|
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 {
|
override fun hashCode(): Int {
|
||||||
|
@ -395,7 +276,11 @@ class RmiClient(private val connection: Connection_,
|
||||||
if (javaClass != other.javaClass) {
|
if (javaClass != other.javaClass) {
|
||||||
return false
|
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)
|
allClasses.remove(clazz)
|
||||||
return allClasses
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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
|
data class ConnectionObjectCreateResponse(val packedIds: Int) : RmiMessage
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
* Internal message to invoke methods remotely.
|
||||||
*/
|
*/
|
||||||
class MethodRequest internal constructor() : RmiMessage {
|
class MethodRequest : RmiMessage {
|
||||||
// the registered kryo ID for the object
|
// if this object was a global or connection specific object
|
||||||
var objectId = 0
|
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
|
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
|
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.Input
|
||||||
import com.esotericsoftware.kryo.io.Output
|
import com.esotericsoftware.kryo.io.Output
|
||||||
import dorkbox.network.connection.KryoExtra
|
import dorkbox.network.connection.KryoExtra
|
||||||
|
import dorkbox.network.rmi.RmiUtils
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal message to invoke methods remotely.
|
* Internal message to invoke methods remotely.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("ConstantConditionIf")
|
||||||
class MethodRequestSerializer : Serializer<MethodRequest>() {
|
class MethodRequestSerializer : Serializer<MethodRequest>() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEBUG = false
|
private const val DEBUG = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun write(kryo: Kryo, output: Output, methodRequest: MethodRequest) {
|
override fun write(kryo: Kryo, output: Output, methodRequest: MethodRequest) {
|
||||||
|
val method = methodRequest.cachedMethod
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
System.err.println("WRITING")
|
System.err.println("WRITING")
|
||||||
System.err.println(":: objectID " + methodRequest.objectId)
|
System.err.println(":: isGlobal ${methodRequest.isGlobal}")
|
||||||
System.err.println(":: methodClassID " + methodRequest.cachedMethod.methodClassId)
|
System.err.println(":: objectID ${methodRequest.objectId}")
|
||||||
System.err.println(":: methodIndex " + methodRequest.cachedMethod.methodIndex)
|
System.err.println(":: methodClassID ${method.methodClassId}")
|
||||||
|
System.err.println(":: methodIndex ${method.methodIndex}")
|
||||||
}
|
}
|
||||||
|
|
||||||
output.writeInt(methodRequest.objectId, true)
|
// we pack objectId + responseId into the same "int", since they are both really shorts (but are represented as ints to make
|
||||||
output.writeInt(methodRequest.cachedMethod.methodClassId, true)
|
// working with them a lot easier
|
||||||
output.writeByte(methodRequest.cachedMethod.methodIndex)
|
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]
|
val serializers = method.serializers
|
||||||
if (serializer != null) {
|
if (serializers.isNotEmpty()) {
|
||||||
kryo.writeObjectOrNull(output, args!![i], serializer)
|
val args = methodRequest.args!!
|
||||||
} else {
|
|
||||||
kryo.writeClassAndObject(output, args!![i])
|
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 {
|
override fun read(kryo: Kryo, input: Input, type: Class<out MethodRequest>): MethodRequest {
|
||||||
val objectID = input.readInt(true)
|
val objectIdRmiId = input.readInt(true)
|
||||||
val methodClassID = input.readInt(true)
|
val objectId = RmiUtils.unpackLeft(objectIdRmiId)
|
||||||
val methodIndex = input.readByte()
|
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) {
|
if (DEBUG) {
|
||||||
System.err.println("READING")
|
System.err.println("READING")
|
||||||
System.err.println(":: objectID $objectID")
|
System.err.println(":: isGlobal $isGlobal")
|
||||||
System.err.println(":: methodClassID $methodClassID")
|
System.err.println(":: objectID $objectId")
|
||||||
|
System.err.println(":: methodClassID $methodClassId")
|
||||||
System.err.println(":: methodIndex $methodIndex")
|
System.err.println(":: methodIndex $methodIndex")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(kryo as KryoExtra)
|
||||||
|
|
||||||
val cachedMethod = try {
|
val cachedMethod = try {
|
||||||
(kryo as KryoExtra).serializationManager.getMethods(methodClassID)[methodIndex.toInt()]
|
kryo.getMethods(methodClassId)[methodIndex]
|
||||||
} catch (ex: Exception) {
|
} 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)
|
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)
|
// this is specifically when we override an interface method, with an implementation method + Connection parameter (@ index 0)
|
||||||
argStartIndex = 1
|
argStartIndex = 1
|
||||||
args = arrayOfNulls<Any>(serializers.size + 1) as Array<Any>
|
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
|
args[0] = kryo.connection
|
||||||
} else {
|
} else {
|
||||||
method = cachedMethod.method
|
method = cachedMethod.method
|
||||||
|
@ -118,28 +138,29 @@ class MethodRequestSerializer : Serializer<MethodRequest>() {
|
||||||
|
|
||||||
val parameterTypes = method.parameterTypes
|
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")
|
// 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
|
var index = 0
|
||||||
val n = serializers.size
|
val size = serializers.size
|
||||||
var j = argStartIndex
|
var argStart = argStartIndex
|
||||||
|
|
||||||
while (i < n) {
|
while (index < size) {
|
||||||
val serializer = serializers[i]
|
val serializer = serializers[index]
|
||||||
if (serializer != null) {
|
if (serializer != null) {
|
||||||
args[j] = kryo.readObjectOrNull(input, parameterTypes[i], serializer)
|
args[argStart] = kryo.readObjectOrNull(input, parameterTypes[index], serializer)
|
||||||
} else {
|
} else {
|
||||||
args[j] = kryo.readClassAndObject(input)
|
args[argStart] = kryo.readClassAndObject(input)
|
||||||
}
|
}
|
||||||
i++
|
index++
|
||||||
j++
|
argStart++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val invokeMethod = MethodRequest()
|
val invokeMethod = MethodRequest()
|
||||||
invokeMethod.objectId = objectID
|
invokeMethod.isGlobal = isGlobal
|
||||||
|
invokeMethod.objectId = objectId
|
||||||
invokeMethod.cachedMethod = cachedMethod
|
invokeMethod.cachedMethod = cachedMethod
|
||||||
invokeMethod.args = args
|
invokeMethod.args = args
|
||||||
invokeMethod.responseId = input.readByte()
|
invokeMethod.responseId = responseId
|
||||||
|
|
||||||
return invokeMethod
|
return invokeMethod
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,12 +38,14 @@ package dorkbox.network.rmi.messages
|
||||||
* Internal message to return the result of a remotely invoked method.
|
* Internal message to return the result of a remotely invoked method.
|
||||||
*/
|
*/
|
||||||
class MethodResponse : RmiMessage {
|
class MethodResponse : RmiMessage {
|
||||||
// the registered kryo ID for the object
|
// if this object was a global or connection specific object
|
||||||
var objectId = 0
|
var isGlobal: Boolean = false
|
||||||
|
|
||||||
// A value of 0 means to not respond (this object is NOT created if the request 'responseId = 0'
|
// the registered kryo ID for the object
|
||||||
// This is just an ID to match requests <-> responses
|
var objectId: Int = 0
|
||||||
var responseId: Byte = 0
|
|
||||||
|
// ID to match requests <-> responses
|
||||||
|
var responseId: Int = 0
|
||||||
|
|
||||||
// this is the result of the invoked method
|
// this is the result of the invoked method
|
||||||
var result: Any? = null
|
var result: Any? = null
|
||||||
|
|
|
@ -19,18 +19,22 @@ import com.esotericsoftware.kryo.Kryo
|
||||||
import com.esotericsoftware.kryo.Serializer
|
import com.esotericsoftware.kryo.Serializer
|
||||||
import com.esotericsoftware.kryo.io.Input
|
import com.esotericsoftware.kryo.io.Input
|
||||||
import com.esotericsoftware.kryo.io.Output
|
import com.esotericsoftware.kryo.io.Output
|
||||||
|
import dorkbox.network.rmi.RmiUtils
|
||||||
|
|
||||||
class MethodResponseSerializer() : Serializer<MethodResponse>() {
|
class MethodResponseSerializer() : Serializer<MethodResponse>() {
|
||||||
override fun write(kryo: Kryo, output: Output, methodResponse: MethodResponse) {
|
override fun write(kryo: Kryo, output: Output, response: MethodResponse) {
|
||||||
output.writeInt(methodResponse.objectId, true)
|
output.writeInt(RmiUtils.packShorts(response.objectId, response.responseId), true)
|
||||||
output.writeByte(methodResponse.responseId)
|
output.writeBoolean(response.isGlobal)
|
||||||
kryo.writeClassAndObject(output, methodResponse.result)
|
kryo.writeClassAndObject(output, response.result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun read(kryo: Kryo, input: Input, type: Class<out MethodResponse>): MethodResponse {
|
override fun read(kryo: Kryo, input: Input, type: Class<out MethodResponse>): MethodResponse {
|
||||||
|
val packedInfo = input.readInt(true)
|
||||||
|
|
||||||
val response = MethodResponse()
|
val response = MethodResponse()
|
||||||
response.objectId = input.readInt(true)
|
response.objectId = RmiUtils.unpackLeft(packedInfo)
|
||||||
response.responseId = input.readByte()
|
response.responseId = RmiUtils.unpackRight(packedInfo)
|
||||||
|
response.isGlobal = input.readBoolean()
|
||||||
response.result = kryo.readClassAndObject(input)
|
response.result = kryo.readClassAndObject(input)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -50,17 +50,19 @@ import dorkbox.network.connection.KryoExtra
|
||||||
class ObjectResponseSerializer(private val rmiImplToIface: IdentityMap<Class<*>, Class<*>>) : Serializer<Any>(false) {
|
class ObjectResponseSerializer(private val rmiImplToIface: IdentityMap<Class<*>, Class<*>>) : Serializer<Any>(false) {
|
||||||
override fun write(kryo: Kryo, output: Output, `object`: Any) {
|
override fun write(kryo: Kryo, output: Output, `object`: Any) {
|
||||||
val kryoExtra = kryo as KryoExtra
|
val kryoExtra = kryo as KryoExtra
|
||||||
val id = kryoExtra.rmiSupport.getRegisteredId(`object`) //
|
// val id = kryoExtra.rmiSupport.getRegisteredId(`object`) //
|
||||||
output.writeInt(id, true)
|
// 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 kryoExtra = kryo as KryoExtra
|
||||||
val objectID = input.readInt(true)
|
val objectID = input.readInt(true)
|
||||||
|
|
||||||
// We have to lookup the iface, since the proxy object requires it
|
// We have to lookup the iface, since the proxy object requires it
|
||||||
val iface = rmiImplToIface.get(implementationType)
|
val iface = rmiImplToIface.get(implementationType)
|
||||||
val connection = kryoExtra.connection
|
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 com.esotericsoftware.kryo.io.Output
|
||||||
import dorkbox.network.connection.KryoExtra
|
import dorkbox.network.connection.KryoExtra
|
||||||
import dorkbox.network.rmi.RmiClient
|
import dorkbox.network.rmi.RmiClient
|
||||||
import org.slf4j.Logger
|
|
||||||
import java.lang.reflect.Proxy
|
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) {
|
override fun write(kryo: Kryo, output: Output, proxyObject: Any) {
|
||||||
val handler = Proxy.getInvocationHandler(proxyObject) as RmiClient
|
val handler = Proxy.getInvocationHandler(proxyObject) as RmiClient
|
||||||
|
output.writeBoolean(handler.isGlobal)
|
||||||
output.writeInt(handler.rmiObjectId, true)
|
output.writeInt(handler.rmiObjectId, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun read(kryo: Kryo, input: Input, type: Class<*>?): Any? {
|
override fun read(kryo: Kryo, input: Input, type: Class<*>?): Any? {
|
||||||
val objectID = input.readInt(true)
|
val isGlobal = input.readBoolean()
|
||||||
val kryoExtra = kryo as KryoExtra
|
val objectId = input.readInt(true)
|
||||||
|
kryo as KryoExtra
|
||||||
|
|
||||||
val `object` = kryoExtra.rmiSupport.getImplementationObject(objectID)
|
val connection = kryo.connection
|
||||||
if (`object` == null) {
|
return connection.endPoint().rmiSupport.getImplObject(isGlobal, objectId, connection)
|
||||||
logger.error("Unknown object ID in RMI ObjectSpace: {}", objectID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return `object`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -141,13 +141,30 @@ interface NetworkSerializationManager : SerializationManager {
|
||||||
/**
|
/**
|
||||||
* @return true if the remote kryo registration are the same as our own
|
* @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
|
* @return the details of all registration IDs -> Class name used by kryo
|
||||||
*/
|
*/
|
||||||
fun getKryoRegistrationDetails(): ByteArray
|
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
|
* Gets the RMI implementation based on the specified interface
|
||||||
*
|
*
|
||||||
|
|
|
@ -15,20 +15,15 @@
|
||||||
*/
|
*/
|
||||||
package dorkbox.network.serialization
|
package dorkbox.network.serialization
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo
|
import com.esotericsoftware.kryo.*
|
||||||
import com.esotericsoftware.kryo.Registration
|
|
||||||
import com.esotericsoftware.kryo.Serializer
|
|
||||||
import com.esotericsoftware.kryo.SerializerFactory
|
|
||||||
import com.esotericsoftware.kryo.io.Input
|
import com.esotericsoftware.kryo.io.Input
|
||||||
import com.esotericsoftware.kryo.io.Output
|
import com.esotericsoftware.kryo.io.Output
|
||||||
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy
|
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy
|
||||||
import com.esotericsoftware.kryo.util.IdentityMap
|
import com.esotericsoftware.kryo.util.IdentityMap
|
||||||
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
|
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
|
||||||
import dorkbox.network.connection.Connection_
|
|
||||||
import dorkbox.network.connection.KryoExtra
|
import dorkbox.network.connection.KryoExtra
|
||||||
import dorkbox.network.connection.ping.PingMessage
|
import dorkbox.network.connection.ping.PingMessage
|
||||||
import dorkbox.network.rmi.CachedMethod
|
import dorkbox.network.rmi.CachedMethod
|
||||||
import dorkbox.network.rmi.NopRmiConnection
|
|
||||||
import dorkbox.network.rmi.RmiUtils
|
import dorkbox.network.rmi.RmiUtils
|
||||||
import dorkbox.network.rmi.messages.*
|
import dorkbox.network.rmi.messages.*
|
||||||
import dorkbox.objectPool.ObjectPool
|
import dorkbox.objectPool.ObjectPool
|
||||||
|
@ -37,6 +32,7 @@ import dorkbox.util.OS
|
||||||
import io.netty.buffer.ByteBuf
|
import io.netty.buffer.ByteBuf
|
||||||
import io.netty.buffer.Unpooled
|
import io.netty.buffer.Unpooled
|
||||||
import org.agrona.collections.Int2ObjectHashMap
|
import org.agrona.collections.Int2ObjectHashMap
|
||||||
|
import org.objenesis.instantiator.ObjectInstantiator
|
||||||
import org.objenesis.strategy.StdInstantiatorStrategy
|
import org.objenesis.strategy.StdInstantiatorStrategy
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
@ -70,6 +66,26 @@ class Serialization(references: Boolean,
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE = 400
|
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
|
* 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 {
|
fun DEFAULT(references: Boolean = true, factory: SerializerFactory<*>? = null): Serialization {
|
||||||
val serialization = Serialization(references, factory)
|
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(ControlMessage::class.java)
|
||||||
serialization.register(PingMessage::class.java) // TODO this is built into aeron!??!?!?!
|
serialization.register(PingMessage::class.java) // TODO this is built into aeron!??!?!?!
|
||||||
|
|
||||||
|
|
||||||
// TODO: this is for diffie hellmen handshake stuff!
|
// TODO: this is for diffie hellmen handshake stuff!
|
||||||
// serialization.register(IESParameters::class.java, IesParametersSerializer())
|
// serialization.register(IESParameters::class.java, IesParametersSerializer())
|
||||||
// serialization.register(IESWithCipherParameters::class.java, IesWithCipherParametersSerializer())
|
// serialization.register(IESWithCipherParameters::class.java, IesWithCipherParametersSerializer())
|
||||||
|
@ -129,47 +113,15 @@ class Serialization(references: Boolean,
|
||||||
// serialization.register(XECPrivateKey::class.java, XECPrivateKeySerializer())
|
// serialization.register(XECPrivateKey::class.java, XECPrivateKeySerializer())
|
||||||
serialization.register(dorkbox.network.connection.registration.Registration::class.java) // must use full package name!
|
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
|
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 lateinit var logger: Logger
|
||||||
|
|
||||||
private var initialized = false
|
private var initialized = false
|
||||||
private val kryoPool: ObjectPool<KryoExtra>
|
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)
|
// 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.
|
// 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
|
private lateinit var savedRegistrationDetails: ByteArray
|
||||||
|
|
||||||
/// RMI things
|
/// RMI things
|
||||||
|
private val rmiIfaceToInstantiator : Int2ObjectHashMap<ObjectInstantiator<Any>> = Int2ObjectHashMap()
|
||||||
private val rmiIfaceToImpl = IdentityMap<Class<*>, Class<*>>()
|
private val rmiIfaceToImpl = IdentityMap<Class<*>, Class<*>>()
|
||||||
private val rmiImplToIface = 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
|
// the purpose of the method cache, is to accelerate looking up methods for specific class
|
||||||
private val methodCache : Int2ObjectHashMap<Array<CachedMethod>> = Int2ObjectHashMap()
|
private val methodCache : Int2ObjectHashMap<Array<CachedMethod>> = Int2ObjectHashMap()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// reflectASM doesn't work on android
|
// reflectASM doesn't work on android
|
||||||
private val useAsm = !OS.isAndroid()
|
private val useAsm = !OS.isAndroid()
|
||||||
|
|
||||||
|
@ -196,33 +159,81 @@ class Serialization(references: Boolean,
|
||||||
synchronized(this@Serialization) {
|
synchronized(this@Serialization) {
|
||||||
|
|
||||||
// we HAVE to pre-allocate the KRYOs
|
// we HAVE to pre-allocate the KRYOs
|
||||||
val kryo = KryoExtra(this@Serialization)
|
val kryo = KryoExtra(methodCache)
|
||||||
|
|
||||||
|
kryo.instantiatorStrategy = instantiatorStrategy
|
||||||
// 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.references = references
|
kryo.references = references
|
||||||
|
|
||||||
// All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems.
|
// 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)
|
// check to see which interfaces are mapped to RMI (otherwise, the interface requires a serializer)
|
||||||
classesToRegister.forEach { registration ->
|
classesToRegister.forEach { registration ->
|
||||||
registration.register(kryo)
|
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) {
|
if (factory != null) {
|
||||||
kryo.setDefaultSerializer(factory)
|
kryo.setDefaultSerializer(factory)
|
||||||
}
|
}
|
||||||
|
@ -340,12 +351,7 @@ class Serialization(references: Boolean,
|
||||||
/**
|
/**
|
||||||
* There is additional overhead to using RMI.
|
* 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
|
* This enables a "remote endpoint" to access methods and create objects (RMI) for this endpoint.
|
||||||
* 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 is NOT bi-directional, and this endpoint cannot access or create remote objects on the "remote client".
|
* 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(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." }
|
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
|
// rmiIfaceToImpl tells us, "the server" how to create a (requested) remote object
|
||||||
// this MUST BE UNIQUE otherwise unexpected and BAD things can happen.
|
// 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
|
// 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)
|
// correctly and (if not) we are are notified on the initial thread (instead of on the network update thread)
|
||||||
val kryo = kryoPool.take()
|
val kryo = kryoPool.take()
|
||||||
|
// save off the class-resolver, so we can lookup the class <-> id relationships
|
||||||
|
classResolver = kryo.classResolver
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// now MERGE all of the registrations (since we can have registrations overwrite newer/specific registrations based on ID
|
// 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!
|
// 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!
|
// on the "RMI server" (aka, where the object lives) side, there will be an interface + implementation!
|
||||||
methodCache[classRegistration.id] =
|
methodCache[classRegistration.id] =
|
||||||
RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, classRegistration.implClass, 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) {
|
} else if (classRegistration.clazz.isInterface) {
|
||||||
// on the "RMI client"
|
// on the "RMI client"
|
||||||
methodCache[classRegistration.id] =
|
methodCache[classRegistration.id] =
|
||||||
RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, null, 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)
|
// save this as a byte array (so class registration validation during connection handshake is faster)
|
||||||
val buffer = Unpooled.buffer(CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE)
|
val buffer = Unpooled.buffer(CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
kryo.writeCompressed(logger, NOP_CONNECTION, buffer, registrationDetails.toTypedArray())
|
kryo.writeCompressed(logger, buffer, registrationDetails.toTypedArray())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error("Unable to write compressed data for registration details", e)
|
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
|
* @return true if kryo registration is required for all classes sent over the wire
|
||||||
*/
|
*/
|
||||||
override fun verifyKryoRegistration(bytes: ByteArray): Boolean {
|
override fun verifyKryoRegistration(clientBytes: ByteArray): Boolean {
|
||||||
val clientRegistrationData = bytes
|
|
||||||
|
|
||||||
// verify the registration IDs if necessary with our own. The CLIENT does not verify anything, only the server!
|
// verify the registration IDs if necessary with our own. The CLIENT does not verify anything, only the server!
|
||||||
val kryoRegistrationDetails = savedRegistrationDetails
|
val kryoRegistrationDetails = savedRegistrationDetails
|
||||||
val equals = kryoRegistrationDetails.contentEquals(clientRegistrationData)
|
val equals = kryoRegistrationDetails.contentEquals(clientBytes)
|
||||||
if (equals) {
|
if (equals) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// now we need to figure out WHAT was screwed up so we know what to fix
|
// 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.
|
// 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 kryo = takeKryo()
|
||||||
val byteBuf = Unpooled.wrappedBuffer(clientRegistrationData)
|
val byteBuf = Unpooled.wrappedBuffer(clientBytes)
|
||||||
try {
|
try {
|
||||||
var success = true
|
var success = true
|
||||||
@Suppress("UNCHECKED_CAST")
|
@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 lengthServer = classesToRegister.size
|
||||||
val lengthClient = clientClassRegistrations.size
|
val lengthClient = clientClassRegistrations.size
|
||||||
var index = 0
|
var index = 0
|
||||||
|
@ -579,6 +595,31 @@ class Serialization(references: Boolean,
|
||||||
kryoPool.put(kryo)
|
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
|
* 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, "!@#$", "<EFBFBD><EFBFBD><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, "!@#$", "<EFBFBD><EFBFBD><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 {
|
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
|
* In this test the server has two objects in an object space.
|
||||||
* uses the first remote object to get the second remote object.
|
*
|
||||||
|
* 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
|
* 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 ->
|
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...
|
// 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> {
|
// connection.create(TestObject::class.java, object : RemoteObjectCallback<TestObject> {
|
||||||
override fun created(remoteObject: TestObject) {
|
// override suspend fun created(remoteObject: TestObject) {
|
||||||
// MUST run on a separate thread because remote object method invocations are blocking
|
// // MUST run on a separate thread because remote object method invocations are blocking
|
||||||
object : Thread() {
|
// object : Thread() {
|
||||||
override fun run() {
|
// override fun run() {
|
||||||
remoteObject.setOther(43.21f)
|
// remoteObject.setOther(43.21f)
|
||||||
|
//
|
||||||
// Normal remote method call.
|
// // Normal remote method call.
|
||||||
Assert.assertEquals(43.21f, remoteObject.other(), .0001f)
|
// Assert.assertEquals(43.21f, remoteObject.other(), .0001f)
|
||||||
|
//
|
||||||
// Make a remote method call that returns another remote proxy object.
|
// // 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.
|
// // the "test" object exists in the REMOTE side, as does the "OtherObject" that is created.
|
||||||
// here we have a proxy to both of them.
|
// // here we have a proxy to both of them.
|
||||||
val otherObject = remoteObject.getOtherObject()
|
// val otherObject = remoteObject.getOtherObject()
|
||||||
|
//
|
||||||
// Normal remote method call on the second object.
|
// // Normal remote method call on the second object.
|
||||||
otherObject.setValue(12.34f)
|
// otherObject.setValue(12.34f)
|
||||||
val value = otherObject.value()
|
// val value = otherObject.value()
|
||||||
Assert.assertEquals(12.34f, value, .0001f)
|
// 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
|
// // 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.
|
// // that is where that object actually exists.
|
||||||
runBlocking {
|
// runBlocking {
|
||||||
connection.send(otherObject)
|
// connection.send(otherObject)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}.start()
|
// }.start()
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|
|
@ -104,32 +104,32 @@ class RmiSendObjectTest : BaseTest() {
|
||||||
val client = Client<Connection>(configuration)
|
val client = Client<Connection>(configuration)
|
||||||
addEndPoint(client)
|
addEndPoint(client)
|
||||||
client.onConnect { connection ->
|
client.onConnect { connection ->
|
||||||
connection.createRemoteObject(TestObject::class.java, object : RemoteObjectCallback<TestObject> {
|
// connection.create(TestObject::class.java, object : RemoteObjectCallback<TestObject> {
|
||||||
override fun created(remoteObject: TestObject) {
|
// override suspend fun created(remoteObject: TestObject) {
|
||||||
// MUST run on a separate thread because remote object method invocations are blocking
|
// // MUST run on a separate thread because remote object method invocations are blocking
|
||||||
object : Thread() {
|
// object : Thread() {
|
||||||
override fun run() {
|
// override fun run() {
|
||||||
remoteObject.setOther(43.21f)
|
// remoteObject.setOther(43.21f)
|
||||||
|
//
|
||||||
// Normal remote method call.
|
// // Normal remote method call.
|
||||||
Assert.assertEquals(43.21f, remoteObject.other(), 0.0001f)
|
// Assert.assertEquals(43.21f, remoteObject.other(), 0.0001f)
|
||||||
|
//
|
||||||
// Make a remote method call that returns another remote proxy object.
|
// // Make a remote method call that returns another remote proxy object.
|
||||||
val otherObject = remoteObject.getOtherObject()
|
// val otherObject = remoteObject.getOtherObject()
|
||||||
|
//
|
||||||
// Normal remote method call on the second object.
|
// // Normal remote method call on the second object.
|
||||||
otherObject.setValue(12.34f)
|
// otherObject.setValue(12.34f)
|
||||||
val value = otherObject.value()
|
// val value = otherObject.value()
|
||||||
Assert.assertEquals(12.34f, value, 0.0001f)
|
// Assert.assertEquals(12.34f, value, 0.0001f)
|
||||||
|
//
|
||||||
// When a remote proxy object is sent, the other side receives its actual remote object.
|
// // When a remote proxy object is sent, the other side receives its actual remote object.
|
||||||
runBlocking {
|
// runBlocking {
|
||||||
connection.send(otherObject)
|
// connection.send(otherObject)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}.start()
|
// }.start()
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|
|
@ -34,10 +34,7 @@
|
||||||
*/
|
*/
|
||||||
package dorkbox.network.rmi
|
package dorkbox.network.rmi
|
||||||
|
|
||||||
import dorkbox.network.BaseTest
|
import dorkbox.network.*
|
||||||
import dorkbox.network.Client
|
|
||||||
import dorkbox.network.Configuration
|
|
||||||
import dorkbox.network.Server
|
|
||||||
import dorkbox.network.connection.Connection
|
import dorkbox.network.connection.Connection
|
||||||
import dorkbox.network.rmi.classes.MessageWithTestCow
|
import dorkbox.network.rmi.classes.MessageWithTestCow
|
||||||
import dorkbox.network.rmi.classes.TestCow
|
import dorkbox.network.rmi.classes.TestCow
|
||||||
|
@ -85,10 +82,22 @@ class RmiTest : BaseTest() {
|
||||||
Assert.assertTrue(caught)
|
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
|
// 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
|
// calls that ignore the return value
|
||||||
|
@ -99,37 +108,43 @@ class RmiTest : BaseTest() {
|
||||||
|
|
||||||
// exceptions are still dealt with properly
|
// exceptions are still dealt with properly
|
||||||
test.moo("Baa")
|
test.moo("Baa")
|
||||||
test.id()
|
|
||||||
caught = false
|
caught = false
|
||||||
try {
|
try {
|
||||||
test.throwException()
|
test.throwException()
|
||||||
} catch (ex: UnsupportedOperationException) {
|
} catch (ex: IllegalStateException) {
|
||||||
caught = true
|
caught = true
|
||||||
}
|
}
|
||||||
Assert.assertTrue(caught)
|
// exceptions are not caught when async = true!
|
||||||
remoteObject.setAsync(true)
|
Assert.assertFalse(caught)
|
||||||
|
|
||||||
|
|
||||||
// wait for the response to id() EVEN THOUGH IT IS ASYNC?
|
// now enable us to wait for responses
|
||||||
Assert.assertEquals(remoteObjectID, remoteObject.waitForLastResponse())
|
// can ONLY wait for responses if we are ASYNC + enabled waiting!!
|
||||||
Assert.assertEquals(0, test.id().toLong())
|
remoteObject.enableWaitingForResponse(true)
|
||||||
|
|
||||||
val responseID = remoteObject.lastResponseID
|
|
||||||
|
test.id()
|
||||||
// wait for the response to 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
|
// Non-blocking call that errors out
|
||||||
// remoteObject.setTransmitReturnValue(false)
|
|
||||||
test.throwException()
|
test.throwException()
|
||||||
Assert.assertEquals(remoteObject.waitForLastResponse()?.javaClass, UnsupportedOperationException::class.java)
|
Assert.assertEquals(remoteObject.waitForLastResponse()?.javaClass, UnsupportedOperationException::class.java)
|
||||||
|
|
||||||
// Call will time out if non-blocking isn't working properly
|
// Call will time out if non-blocking isn't working properly
|
||||||
// remoteObject.setTransmitExceptions(false)
|
test.moo("Mooooooooo", 4000)
|
||||||
test.moo("Mooooooooo", 3000)
|
|
||||||
|
|
||||||
// should wait for a small time
|
// should wait for a small time
|
||||||
// remoteObject.setTransmitReturnValue(true)
|
remoteObject.async = false
|
||||||
remoteObject.setAsync(false)
|
|
||||||
remoteObject.responseTimeout = 6000
|
remoteObject.responseTimeout = 6000
|
||||||
println("You should see this 2 seconds before")
|
println("You should see this 2 seconds before")
|
||||||
val slow = test.slow()
|
val slow = test.slow()
|
||||||
|
@ -155,7 +170,17 @@ class RmiTest : BaseTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(SecurityException::class, IOException::class, InterruptedException::class)
|
@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()
|
rmi()
|
||||||
|
|
||||||
// have to reset the object ID counter
|
// have to reset the object ID counter
|
||||||
|
@ -166,9 +191,10 @@ class RmiTest : BaseTest() {
|
||||||
@Test
|
@Test
|
||||||
@Throws(SecurityException::class, IOException::class, InterruptedException::class)
|
@Throws(SecurityException::class, IOException::class, InterruptedException::class)
|
||||||
fun rmiIPC() {
|
fun rmiIPC() {
|
||||||
TODO("DO IPC STUFF!")
|
|
||||||
rmi { configuration ->
|
rmi { configuration ->
|
||||||
// configuration.localChannelName = EndPoint.LOCAL_CHANNEL
|
if (configuration is ServerConfiguration) {
|
||||||
|
configuration.listenIpAddress = LOOPBACK
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// have to reset the object ID counter
|
// have to reset the object ID counter
|
||||||
|
@ -200,21 +226,11 @@ class RmiTest : BaseTest() {
|
||||||
System.err.println("Starting test for: Server -> Client")
|
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
|
// 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.createObject<TestCow> { remoteObject ->
|
||||||
connection.createRemoteObject(TestCow::class.java, object : RemoteObjectCallback<TestCow> {
|
System.err.println("Running test for: Server -> Client")
|
||||||
override fun created(remoteObject: TestCow) {
|
runTests(connection, remoteObject, 2)
|
||||||
// MUST run on a separate thread because remote object method invocations are blocking
|
System.err.println("Done with test for: Server -> Client")
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,10 +239,7 @@ class RmiTest : BaseTest() {
|
||||||
config(configuration)
|
config(configuration)
|
||||||
register(configuration.serialization)
|
register(configuration.serialization)
|
||||||
|
|
||||||
// this is for testing the "screwed up registrations logic". It should screwup for both network AND local-JVM connections
|
// for Server -> Client RMI
|
||||||
// configuration.serialization.register(ExtraClassTest1.class);
|
|
||||||
|
|
||||||
// // for Server -> Client RMI
|
|
||||||
configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java)
|
configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java)
|
||||||
val client = Client<Connection>(configuration)
|
val client = Client<Connection>(configuration)
|
||||||
addEndPoint(client)
|
addEndPoint(client)
|
||||||
|
@ -234,24 +247,14 @@ class RmiTest : BaseTest() {
|
||||||
client.onConnect { connection ->
|
client.onConnect { connection ->
|
||||||
System.err.println("Starting test for: Client -> Server")
|
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.createObject<TestCow> { remoteObject ->
|
||||||
connection.createRemoteObject(TestCow::class.java, object : RemoteObjectCallback<TestCow> {
|
System.err.println("Running test for: Client -> Server")
|
||||||
override fun created(remoteObject: TestCow) {
|
runTests(connection, remoteObject, 1)
|
||||||
// MUST run on a separate thread because remote object method invocations are blocking
|
System.err.println("Done with test for: Client -> Server")
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.onMessage<MessageWithTestCow> { connection, m ->
|
client.onMessage<MessageWithTestCow> { _, m ->
|
||||||
System.err.println("Received finish signal for test for: Client -> Server")
|
System.err.println("Received finish signal for test for: Client -> Server")
|
||||||
val `object` = m.testCow
|
val `object` = m.testCow
|
||||||
val id = `object`.id()
|
val id = `object`.id()
|
||||||
|
@ -268,19 +271,73 @@ class RmiTest : BaseTest() {
|
||||||
waitForThreads()
|
waitForThreads()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ExtraClassTest1(foo: Int) {
|
@Throws(SecurityException::class, IOException::class)
|
||||||
var foo = 0
|
fun rmiGlobal(config: (Configuration) -> Unit = {}) {
|
||||||
|
run {
|
||||||
|
val configuration = serverConfig()
|
||||||
|
config(configuration)
|
||||||
|
register(configuration.serialization)
|
||||||
|
|
||||||
init {
|
// for Client -> Server RMI
|
||||||
this.foo = foo
|
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) {
|
run {
|
||||||
var foo = 0
|
val configuration = clientConfig()
|
||||||
|
config(configuration)
|
||||||
|
register(configuration.serialization)
|
||||||
|
|
||||||
init {
|
// for Server -> Client RMI
|
||||||
this.foo = foo
|
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
|
package dorkbox.network.rmi.classes
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class MessageWithTestCow(val testCow: TestCow) {
|
class MessageWithTestCow(val testCow: TestCow) {
|
||||||
var number = 0
|
var number = 0
|
||||||
var text: String? = null
|
var text: String? = null
|
||||||
|
|
|
@ -14,11 +14,9 @@
|
||||||
*/
|
*/
|
||||||
package dorkbox.network.rmi.classes
|
package dorkbox.network.rmi.classes
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
open class TestCowBaseImpl : TestCowBase {
|
open class TestCowBaseImpl : TestCowBase {
|
||||||
override fun throwException() {
|
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?")
|
throw UnsupportedOperationException("Why would I do that?")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue