655 lines
23 KiB
Kotlin
655 lines
23 KiB
Kotlin
/*
|
|
* 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.connection
|
|
|
|
import dorkbox.network.Server
|
|
import dorkbox.network.connection.ping.PingFuture
|
|
import dorkbox.network.connection.ping.PingMessage
|
|
import dorkbox.network.other.NetworkUtil
|
|
import dorkbox.network.rmi.RmiSupportConnection
|
|
import dorkbox.util.classes.ClassHelper
|
|
import io.aeron.FragmentAssembler
|
|
import io.aeron.Publication
|
|
import io.aeron.Subscription
|
|
import io.aeron.logbuffer.FragmentHandler
|
|
import io.aeron.logbuffer.Header
|
|
import kotlinx.atomicfu.atomic
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.runBlocking
|
|
import org.agrona.BitUtil
|
|
import org.agrona.BufferUtil
|
|
import org.agrona.DirectBuffer
|
|
import org.agrona.concurrent.UnsafeBuffer
|
|
import java.io.IOException
|
|
import java.util.concurrent.CountDownLatch
|
|
import java.util.concurrent.TimeUnit
|
|
import java.util.concurrent.atomic.AtomicLong
|
|
import javax.crypto.SecretKey
|
|
|
|
/**
|
|
* The "network connection" is established once the registration is validated for TCP/UDP
|
|
*/
|
|
open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: MediaDriverConnection)
|
|
: Connection_, Listeners<Connection> {
|
|
|
|
private val subscription: Subscription
|
|
private val publication: Publication
|
|
|
|
final override val subscriptionPort: Int
|
|
final override val publicationPort: Int
|
|
final override val remoteAddressInt: Int
|
|
final override val remoteAddress: String
|
|
final override val streamId: Int
|
|
final override val sessionId: Int
|
|
|
|
|
|
private val serialization = endPoint.config.serialization
|
|
private val sendIdleStrategy = endPoint.config.sendIdleStrategy.clone()
|
|
|
|
private val expirationTime = System.currentTimeMillis() +
|
|
TimeUnit.SECONDS.toMillis(endPoint.config.connectionCleanupTimeoutInSeconds.toLong())
|
|
|
|
private val logger = endPoint.logger
|
|
|
|
// private val needsLock = AtomicBoolean(false)
|
|
// private val writeSignalNeeded = AtomicBoolean(false)
|
|
// private val writeLock = Any()
|
|
// private val closeInProgress = AtomicBoolean(false)
|
|
private val isClosed = atomic(false)
|
|
// private val channelIsClosed = AtomicBoolean(false)
|
|
// private val messageInProgressLock = Any()
|
|
// private val messageInProgress = AtomicBoolean(false)
|
|
|
|
|
|
@Volatile
|
|
private var pingFuture: PingFuture? = null
|
|
|
|
// used to store connection local listeners (instead of global listeners). Only possible on the server.
|
|
// @Volatile
|
|
// private var localListenerManager: ConnectionManager<*>? = null
|
|
|
|
// while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error.
|
|
private var remoteKeyChanged = false
|
|
|
|
// The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 8 (external counter) + 4 (GCM counter)
|
|
// The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
|
|
// counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
|
|
private val aes_gcm_iv = AtomicLong(0)
|
|
|
|
// when closing this connection, HOW MANY endpoints need to be closed?
|
|
private var closeLatch: CountDownLatch? = null
|
|
|
|
// RMI support for this connection
|
|
private val rmiSupportConnection: RmiSupportConnection<Connection_>
|
|
|
|
|
|
var messageHandler: FragmentAssembler
|
|
|
|
val buffer = UnsafeBuffer(BufferUtil.allocateDirectAligned(10000, BitUtil.CACHE_LINE_LENGTH))
|
|
|
|
|
|
init {
|
|
// we have to construct how the connection will communicate!
|
|
if (endPoint is Server) {
|
|
mediaDriverConnection.buildServer(endPoint.aeron)
|
|
} else {
|
|
runBlocking {
|
|
mediaDriverConnection.buildClient(endPoint.aeron)
|
|
}
|
|
}
|
|
|
|
logger.debug("creating new connection $mediaDriverConnection")
|
|
|
|
// can only get this AFTER we have built the sub/pub
|
|
subscription = mediaDriverConnection.subscription
|
|
publication = mediaDriverConnection.publication
|
|
|
|
subscriptionPort = mediaDriverConnection.subscriptionPort
|
|
publicationPort = mediaDriverConnection.publicationPort
|
|
remoteAddress = mediaDriverConnection.address
|
|
remoteAddressInt = NetworkUtil.IP.toInt(remoteAddress)
|
|
streamId = mediaDriverConnection.streamId // NOTE: this is UNIQUE per server!
|
|
sessionId = mediaDriverConnection.sessionId
|
|
|
|
messageHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
|
|
// small problem... If we expect IN ORDER messages (ie: setting a value, then later reading the value), multiple threads
|
|
// don't work.
|
|
endPoint.actionDispatch.launch {
|
|
endPoint.readMessage(buffer, offset, length, header, this@ConnectionImpl)
|
|
}
|
|
})
|
|
|
|
rmiSupportConnection = RmiSupportConnection(logger, endPoint.rmiSupport, endPoint.serialization, endPoint.actionDispatch)
|
|
|
|
// when closing this connection, HOW MANY endpoints need to be closed?
|
|
closeLatch = CountDownLatch(1)
|
|
}
|
|
|
|
/**
|
|
* @param now The current time
|
|
*
|
|
* @return `true` if this connection has no subscribers and the current time `now` is after the expriation date
|
|
*/
|
|
override fun isExpired(now: Long): Boolean {
|
|
return subscription.imageCount() == 0 && now > expirationTime
|
|
}
|
|
|
|
override fun pollSubscriptions(): Int {
|
|
return subscription.poll(messageHandler, 1024)
|
|
}
|
|
|
|
/**
|
|
* @return the AES key. key=32 byte, iv=12 bytes (AES-GCM implementation).
|
|
*/
|
|
override fun cryptoKey(): SecretKey {
|
|
TODO()
|
|
// return channelWrapper.cryptoKey()
|
|
}
|
|
|
|
/**
|
|
* This is the per-message sequence number.
|
|
*
|
|
* The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter)
|
|
* The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
|
|
* counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
|
|
*/
|
|
override fun nextGcmSequence(): Long {
|
|
return aes_gcm_iv.getAndIncrement()
|
|
}
|
|
|
|
/**
|
|
* Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed.
|
|
*/
|
|
override fun hasRemoteKeyChanged(): Boolean {
|
|
return remoteKeyChanged
|
|
}
|
|
|
|
override val isLoopback: Boolean
|
|
get() = TODO("Not yet implemented")
|
|
override val isIPC: Boolean
|
|
get() = TODO("Not yet implemented")
|
|
override val isNetwork: Boolean
|
|
get() = TODO("Not yet implemented")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Updates the ping times for this connection (called when this connection gets a REPLY ping message).
|
|
*/
|
|
fun updatePingResponse(ping: PingMessage?) {
|
|
if (pingFuture != null) {
|
|
pingFuture!!.setSuccess(this, ping)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a "ping" packet, trying UDP then TCP (in that order) to measure **ROUND TRIP** time to the remote connection.
|
|
*
|
|
* @return Ping can have a listener attached, which will get called when the ping returns.
|
|
*/
|
|
override suspend fun ping(): Ping {
|
|
// val pingFuture2 = pingFuture
|
|
// if (pingFuture2 != null && !pingFuture2.isSuccess) {
|
|
// pingFuture2.cancel()
|
|
// }
|
|
// val newPromise: Promise<PingTuple<out Connection?>>
|
|
// newPromise = if (channelWrapper.udp() != null) {
|
|
// channelWrapper.udp()
|
|
// .newPromise()
|
|
// } else {
|
|
// channelWrapper.tcp()
|
|
// .newPromise()
|
|
// }
|
|
// pingFuture = PingFuture(newPromise)
|
|
// val ping = PingMessage()
|
|
// ping.id = pingFuture!!.id
|
|
// ping0(ping)
|
|
// return pingFuture!!
|
|
TODO()
|
|
}
|
|
|
|
/**
|
|
* INTERNAL USE ONLY. Used to initiate a ping, and to return a ping.
|
|
*
|
|
* Sends a ping message attempted in the following order: UDP, TCP,LOCAL
|
|
*/
|
|
fun ping0(ping: PingMessage) {
|
|
// if (channelWrapper.udp() != null) {
|
|
// UDP(ping)
|
|
// } else if (channelWrapper.tcp() != null) {
|
|
// TCP(ping)
|
|
// } else {
|
|
// self(ping)
|
|
// }
|
|
TODO()
|
|
}
|
|
|
|
/**
|
|
* Returns the last calculated TCP return trip time, or -1 if or the [PingMessage] response has not yet been received.
|
|
*/
|
|
val lastRoundTripTime: Int
|
|
get() {
|
|
val pingFuture2 = pingFuture
|
|
return pingFuture2?.response ?: -1
|
|
}
|
|
|
|
/**
|
|
* needed to place back-pressure when writing too much data to the connection.
|
|
*
|
|
* This blocks until we are writable again
|
|
*/
|
|
// TODO: remove this!?!? use idle backoff strategy?!?
|
|
// fun controlBackPressure(c: ConnectionPoint) {
|
|
// while (!closeInProgress.get() && !c.isWritable) {
|
|
// needsLock.set(true)
|
|
// writeSignalNeeded.set(true)
|
|
// synchronized(writeLock) {
|
|
// if (needsLock.get()) {
|
|
// try {
|
|
// // waits 1 second maximum per check. This is to guarantee that eventually (in the case of deadlocks, which i've seen)
|
|
// // it will get released. The while loop makes sure it will exit when the channel is writable
|
|
//// writeLock.wait(1000)
|
|
// } catch (e: InterruptedException) {
|
|
// e.printStackTrace()
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
/**
|
|
* Send the given message to the given publication. If the publication fails to accept the message, the method will retry `5` times,
|
|
* waiting `100` milliseconds each time, before throwing an exception.
|
|
*
|
|
* @param pub The publication
|
|
* @param buffer A buffer that will hold the message for sending
|
|
* @param message The message
|
|
*
|
|
* @return The new publication stream position
|
|
*
|
|
* @throws IOException If the message cannot be sent
|
|
*/
|
|
|
|
/**
|
|
* Safely sends objects to a destination (such as a custom object or a standard ping).
|
|
*/
|
|
override suspend fun send(message: Any) {
|
|
// The sessionId is globally unique, and is assigned by the server.
|
|
logger.debug("[{}] send: {}", publication.sessionId(), message)
|
|
|
|
val kryo: KryoExtra = serialization.takeKryo()
|
|
try {
|
|
kryo.write(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)
|
|
}
|
|
}
|
|
|
|
|
|
override suspend fun send(message: Any, priority: Byte) {
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Closes the connection, and removes all connection specific listeners
|
|
*/
|
|
override fun close() {
|
|
if (isClosed.compareAndSet(expect = false, update = true)) {
|
|
subscription.close()
|
|
publication.close()
|
|
}
|
|
|
|
// only close if we aren't already in the middle of closing.
|
|
// if (closeInProgress.compareAndSet(false, true)) {
|
|
// val idleTimeoutMs = 2000
|
|
//
|
|
// // if we are in the middle of a message, hold off.
|
|
//// synchronized(messageInProgressLock) {
|
|
//// // while loop is to prevent spurious wakeups!
|
|
//// while (messageInProgress.get()) {
|
|
//// try {
|
|
////// messageInProgressLock.wait(idleTimeoutMs.toLong())
|
|
//// } catch (ignored: InterruptedException) {
|
|
//// }
|
|
//// }
|
|
//// }
|
|
//
|
|
//
|
|
// // close out the ping future
|
|
// val pingFuture2 = pingFuture
|
|
// pingFuture2?.cancel()
|
|
// pingFuture = null
|
|
//
|
|
//// synchronized(channelIsClosed) {
|
|
//// if (!channelIsClosed.get()) {
|
|
//// // this will have netty call "channelInactive()"
|
|
////// channelWrapper.close(this, sessionManager, false)
|
|
////
|
|
//// // want to wait for the "channelInactive()" method to FINISH ALL TYPES before allowing our current thread to continue!
|
|
//// try {
|
|
//// closeLatch!!.await(idleTimeoutMs.toLong(), TimeUnit.MILLISECONDS)
|
|
//// } catch (ignored: InterruptedException) {
|
|
//// }
|
|
//// }
|
|
//// }
|
|
//
|
|
// // remove all listeners AFTER we close the channel.
|
|
// if (!keepListeners) {
|
|
// removeAll()
|
|
// }
|
|
//
|
|
//
|
|
// // remove all RMI listeners
|
|
// rmiSupport!!.close()
|
|
// }
|
|
|
|
// remove all listeners AFTER we close the channel.
|
|
// if (!keepListeners) {
|
|
removeAll()
|
|
// }
|
|
|
|
|
|
// remove all RMI listeners
|
|
// rmiSupport.close() // TODO
|
|
}
|
|
|
|
|
|
// @Throws(Exception::class)
|
|
// override fun exceptionCaught(context: ChannelHandlerContext, cause: Throwable) {
|
|
// val channel = context.channel()
|
|
// if (cause !is IOException) {
|
|
// // safe to ignore, since it's thrown when we try to interact with a closed socket. Race conditions cause this, and
|
|
// // it is still safe to ignore.
|
|
// logger!!.error("Unexpected exception while receiving data from {}", channel.remoteAddress(), cause)
|
|
//
|
|
// // the ONLY sockets that can call this are:
|
|
// // CLIENT TCP or UDP
|
|
// // SERVER TCP
|
|
// if (channel.isOpen) {
|
|
// channel.close()
|
|
// }
|
|
// } else {
|
|
// // it's an IOException, just log it!
|
|
// logger!!.error("Unexpected exception while communicating with {}!", channel.remoteAddress(), cause)
|
|
// }
|
|
// }
|
|
|
|
// /**
|
|
// * Expose methods to modify the connection listeners.
|
|
// */
|
|
// override fun listeners(): Listeners<Connection> {
|
|
// return this
|
|
// }
|
|
|
|
/**
|
|
* Adds a listener to this connection/endpoint to be notified of
|
|
* connect/disconnect/idle/receive(object) events.
|
|
*
|
|
*
|
|
* If the listener already exists, it is not added again.
|
|
*
|
|
*
|
|
* When called by a server, NORMALLY listeners are added at the GLOBAL level
|
|
* (meaning, I add one listener, and ALL connections are notified of that
|
|
* listener.
|
|
*
|
|
*
|
|
* It is POSSIBLE to add a server connection ONLY (ie, not global) listener
|
|
* (via connection.addListener), meaning that ONLY that listener attached to
|
|
* the connection is notified on that event (ie, admin type listeners)
|
|
*/
|
|
|
|
|
|
override fun filter(function: (Connection) -> Boolean): Listeners<Connection> {
|
|
return this
|
|
}
|
|
|
|
override fun onConnect(function: (Connection) -> Unit): Listeners<Connection> {
|
|
return this
|
|
}
|
|
|
|
override fun onDisconnect(function: (Connection) -> Unit): Listeners<Connection> {
|
|
return this
|
|
}
|
|
|
|
override fun onError(function: (Connection, throwable: Throwable) -> Unit): Listeners<Connection> {
|
|
return this
|
|
}
|
|
|
|
override fun <M : Any> onMessage(function: (Connection, M) -> Unit): Listeners<Connection> {
|
|
return this
|
|
}
|
|
|
|
// override fun add(listener: OnConnected<Connection>): Listeners<Connection> {
|
|
// if (endPoint is EndPointServer) {
|
|
// // when we are a server, NORMALLY listeners are added at the GLOBAL level
|
|
// // meaning --
|
|
// // I add one listener, and ALL connections are notified of that listener.
|
|
// //
|
|
// // HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
|
// // that listener is notified on that event (ie, admin type listeners)
|
|
//
|
|
// // synchronized because this should be VERY uncommon, and we want to make sure that when the manager
|
|
// // is empty, we can remove it from this connection.
|
|
//// synchronized(this) {
|
|
//// if (localListenerManager == null) {
|
|
//// localListenerManager = endPoint.addListenerManager(this)
|
|
//// }
|
|
//// localListenerManager!!.add(listener)
|
|
//// }
|
|
// } else {
|
|
//// endPoint.listeners()
|
|
//// .add(listener)
|
|
// }
|
|
// return this
|
|
// }
|
|
|
|
/**
|
|
* Removes a listener from this connection/endpoint to NO LONGER be notified
|
|
* of connect/disconnect/idle/receive(object) events.
|
|
*
|
|
*
|
|
* When called by a server, NORMALLY listeners are added at the GLOBAL level
|
|
* (meaning, I add one listener, and ALL connections are notified of that
|
|
* listener.
|
|
*
|
|
*
|
|
* It is POSSIBLE to remove a server-connection 'non-global' listener (via
|
|
* connection.removeListener), meaning that ONLY that listener attached to
|
|
* the connection is removed
|
|
*/
|
|
override fun remove(listener: OnConnected<Connection>): Listeners<Connection> {
|
|
if (endPoint is Server) {
|
|
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
|
// meaning --
|
|
// I add one listener, and ALL connections are notified of that listener.
|
|
//
|
|
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
|
// that listener is notified on that event (ie, admin type listeners)
|
|
|
|
// synchronized because this should be uncommon, and we want to make sure that when the manager
|
|
// is empty, we can remove it from this connection.
|
|
// synchronized(this) {
|
|
// val local = localListenerManager
|
|
// if (local != null) {
|
|
// local.remove(listener)
|
|
// if (!local.hasListeners()) {
|
|
// endPoint.removeListenerManager(this)
|
|
// }
|
|
// }
|
|
// }
|
|
} else {
|
|
// endPoint.listeners()
|
|
// .remove(listener)
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Removes all registered listeners from this connection/endpoint to NO
|
|
* LONGER be notified of connect/disconnect/idle/receive(object) events.
|
|
*
|
|
* This includes all proxy listeners
|
|
*/
|
|
override fun removeAll(): Listeners<Connection> {
|
|
// rmiSupport.removeAllListeners() // TODO
|
|
|
|
if (endPoint is Server) {
|
|
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
|
// meaning --
|
|
// I add one listener, and ALL connections are notified of that listener.
|
|
//
|
|
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
|
// that listener is notified on that event (ie, admin type listeners)
|
|
|
|
// synchronized because this should be uncommon, and we want to make sure that when the manager
|
|
// is empty, we can remove it from this connection.
|
|
// synchronized(this) {
|
|
// if (localListenerManager != null) {
|
|
// localListenerManager?.removeAll()
|
|
// localListenerManager = null
|
|
// endPoint.removeListenerManager(this)
|
|
// }
|
|
// }
|
|
} else {
|
|
// endPoint.listeners()
|
|
// .removeAll()
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Removes all registered listeners (of the object type) from this connection/endpoint to NO LONGER be notified of
|
|
* connect/disconnect/idle/receive(object) events.
|
|
*/
|
|
override fun removeAll(classType: Class<*>): Listeners<Connection> {
|
|
if (endPoint is Server) {
|
|
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
|
// meaning --
|
|
// I add one listener, and ALL connections are notified of that listener.
|
|
//
|
|
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
|
// that listener is notified on that event (ie, admin type listeners)
|
|
|
|
// synchronized because this should be uncommon, and we want to make sure that when the manager
|
|
// is empty, we can remove it from this connection.
|
|
// synchronized(this) {
|
|
// val local = localListenerManager
|
|
// if (local != null) {
|
|
// local.removeAll(classType)
|
|
// if (!local.hasListeners()) {
|
|
// localListenerManager = null
|
|
// endPoint.removeListenerManager(this)
|
|
// }
|
|
// }
|
|
// }
|
|
} else {
|
|
// endPoint.listeners()
|
|
// .removeAll(classType)
|
|
}
|
|
return this
|
|
}
|
|
|
|
|
|
override fun isClosed(): Boolean {
|
|
return false
|
|
}
|
|
|
|
override fun endPoint(): EndPoint<*> {
|
|
return endPoint
|
|
}
|
|
|
|
|
|
override fun toString(): String {
|
|
// return channelWrapper.toString()
|
|
return "TODO"
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
return sessionId
|
|
}
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) {
|
|
return true
|
|
}
|
|
if (other == null) {
|
|
return false
|
|
}
|
|
if (javaClass != other.javaClass) {
|
|
return false
|
|
}
|
|
// val other1 = other as ConnectionImpl
|
|
// if (channelWrapper != other1.channelWrapper) {
|
|
// return false
|
|
// }
|
|
return true
|
|
}
|
|
|
|
//
|
|
//
|
|
// RMI methods
|
|
//
|
|
//
|
|
override fun rmiSupport(): RmiSupportConnection<Connection_> {
|
|
return rmiSupportConnection
|
|
}
|
|
|
|
override suspend fun <Iface> createObject(callback: suspend (Iface) -> Unit) {
|
|
val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function2::class.java, callback.javaClass, 0)
|
|
val interfaceClassId = endPoint.serialization.getClassId(iFaceClass)
|
|
rmiSupportConnection.createRemoteObject(this, interfaceClassId, callback)
|
|
}
|
|
|
|
override fun <Iface> getObject(objectId: Int, interfaceClass: Class<Iface>): Iface {
|
|
@Suppress("UNCHECKED_CAST")
|
|
return rmiSupportConnection.getRemoteObject(this, endPoint as EndPoint<Connection_>, objectId, interfaceClass)
|
|
}
|
|
}
|