Network/src/dorkbox/network/Client.kt

950 lines
42 KiB
Kotlin
Raw Normal View History

/*
2020-08-19 15:29:35 +02:00
* 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
import dorkbox.bytes.toHexString
import dorkbox.dns.DnsClient
import dorkbox.netUtil.IP
import dorkbox.netUtil.IPv4
import dorkbox.netUtil.IPv6
import dorkbox.netUtil.dnsUtils.ResolvedAddressTypes
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.mediaDriver.ClientIpcDriver
import dorkbox.network.aeron.mediaDriver.ClientUdpDriver
import dorkbox.network.aeron.mediaDriver.MediaDriverClient
import dorkbox.network.aeron.mediaDriver.MediaDriverConnectInfo
import dorkbox.network.connection.Connection
import dorkbox.network.connection.ConnectionParams
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.ListenerManager
import dorkbox.network.connection.PublicKeyValidationState
2020-09-09 01:33:09 +02:00
import dorkbox.network.exceptions.ClientException
import dorkbox.network.exceptions.ClientRejectedException
import dorkbox.network.exceptions.ClientRetryException
import dorkbox.network.exceptions.ClientShutdownException
2020-09-09 01:33:09 +02:00
import dorkbox.network.exceptions.ClientTimedOutException
import dorkbox.network.exceptions.ServerException
import dorkbox.network.handshake.ClientHandshake
import dorkbox.network.ping.Ping
import dorkbox.network.ping.PingManager
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
2022-06-15 23:53:26 +02:00
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.lang.Thread.sleep
2020-09-09 01:33:09 +02:00
import java.net.Inet4Address
import java.net.Inet6Address
2020-09-09 01:33:09 +02:00
import java.net.InetAddress
import java.util.concurrent.*
/**
* The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's
* ASYNC.
*
* @param config these are the specific connection options
* @param connectionFunc allows for custom connection implementations defined as a unit function
2022-05-28 12:15:45 +02:00
* @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints)
*/
@Suppress("unused")
open class Client<CONNECTION : Connection>(
config: ClientConfiguration = ClientConfiguration(),
2022-05-28 12:15:45 +02:00
connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION,
loggerName: String = Client::class.java.simpleName)
: EndPoint<CONNECTION>(config, connectionFunc, loggerName) {
/**
* The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's
* ASYNC.
*
* @param config these are the specific connection options
* @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints)
* @param connectionFunc allows for custom connection implementations defined as a unit function
*/
constructor(config: ClientConfiguration,
loggerName: String,
connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION)
: this(config, connectionFunc, loggerName)
2022-05-28 12:15:45 +02:00
/**
* The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's
* ASYNC.
*
* @param config these are the specific connection options
* @param connectionFunc allows for custom connection implementations defined as a unit function
*/
constructor(config: ClientConfiguration,
2022-05-28 12:15:45 +02:00
connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION)
: this(config, connectionFunc, Client::class.java.simpleName)
/**
* The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's
* ASYNC.
*
* @param config these are the specific connection options
* @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints)
*/
constructor(config: ClientConfiguration,
2022-05-28 12:15:45 +02:00
loggerName: String)
: this(config,
{
@Suppress("UNCHECKED_CAST")
Connection(it) as CONNECTION
},
loggerName)
/**
* The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's
* ASYNC.
*
* @param config these are the specific connection options
*/
constructor(config: ClientConfiguration)
2022-05-28 12:15:45 +02:00
: this(config,
{
@Suppress("UNCHECKED_CAST")
Connection(it) as CONNECTION
},
Client::class.java.simpleName)
companion object {
/**
* Gets the version number.
*/
2023-02-14 23:47:04 +01:00
const val version = "6.4"
2021-04-09 20:24:45 +02:00
/**
* Checks to see if a client (using the specified configuration) is running.
*
* This method should only be used to check if a client is running for a DIFFERENT configuration than the currently running client
*/
fun isRunning(configuration: Configuration): Boolean {
return AeronDriver(configuration).isRunning()
}
2021-04-09 20:24:45 +02:00
init {
// Add this project to the updates system, which verifies this class + UUID + version information
2021-04-27 14:00:31 +02:00
dorkbox.updates.Updates.add(Client::class.java, "5be42ae40cac49fb90dea86bc513141b", version)
2021-04-09 20:24:45 +02:00
}
}
/**
* The network or IPC address for the client to connect to.
*
* For a network address, it can be:
* - a network name ("localhost", "loopback", "lo", "bob.example.org")
* - an IP address ("127.0.0.1", "123.123.123.123", "::1")
*
* For the IPC (Inter-Process-Communication) address. it must be:
* - the IPC integer ID, "0x1337c0de", "0x12312312", etc.
*/
@Volatile
var remoteAddress: InetAddress? = IPv4.LOCALHOST
private set
/**
* the remote address, as a string.
*/
@Volatile
var remoteAddressString: String = "UNKNOWN"
private set
@Volatile
private var isConnected = false
// is valid when there is a connection to the server, otherwise it is null
private var connection0: CONNECTION? = null
// This is set by the client so if there is a "connect()" call in the the disconnect callback, we can have proper
// lock-stop ordering for how disconnect and connect work with each-other
// GUARANTEE that the callbacks for 'onDisconnect' happens-before the 'onConnect'.
2022-06-15 23:53:26 +02:00
private val lockStepForConnect = atomic<Mutex?>(null)
final override fun newException(message: String, cause: Throwable?): Throwable {
return ClientException(message, cause)
}
/**
* Will attempt to connect to the server, with a default 30 second connection timeout and will block until completed.
*
* Default connection is to localhost
*
* ### For a network address, it can be:
* - a network name ("localhost", "bob.example.org")
* - an IP address ("127.0.0.1", "123.123.123.123", "::1")
* - an InetAddress address
*
* ### For the IPC (Inter-Process-Communication) it must be:
* - `connect()`
* - `connect("")`
* - `connectIpc()`
*
* ### Case does not matter, and "localhost" is the default.
*
* @param remoteAddress The network or if localhost, IPC address for the client to connect to
* @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely
* @param reliable true if we want to create a reliable connection (for UDP connections, is message loss acceptable?).
*
* @throws IllegalArgumentException if the remote address is invalid
* @throws ClientTimedOutException if the client is unable to connect in x amount of time
* @throws ClientRejectedException if the client connection is rejected
*/
2022-07-15 06:58:49 +02:00
fun connect(
remoteAddress: InetAddress,
connectionTimeoutSec: Int = 30,
reliable: Boolean = true)
{
val remoteAddressString = when (remoteAddress) {
is Inet4Address -> IPv4.toString(remoteAddress)
is Inet6Address -> IPv6.toString(remoteAddress, true)
2022-12-17 22:24:33 +01:00
else -> throw IllegalArgumentException("Cannot connect to $remoteAddress It is an invalid address type!")
}
// Default IPC ports are flipped because they are in the perspective of the SERVER
connect(remoteAddress = remoteAddress,
remoteAddressString = remoteAddressString,
2022-12-17 22:24:33 +01:00
remoteAddressPrettyString = remoteAddressString,
connectionTimeoutSec = connectionTimeoutSec,
reliable = reliable)
}
/**
* Will attempt to connect to the server via IPC, with a default 30 second connection timeout and will block until completed.
*
* @param ipcId The IPC address for the client to connect to
* @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely.
*
* @throws IllegalArgumentException if the remote address is invalid
* @throws ClientTimedOutException if the client is unable to connect in x amount of time
* @throws ClientRejectedException if the client connection is rejected
*/
@Suppress("DuplicatedCode")
2022-07-15 06:58:49 +02:00
fun connectIpc(
ipcId: Int = AeronDriver.IPC_HANDSHAKE_STREAM_ID,
2022-07-15 06:58:49 +02:00
connectionTimeoutSec: Int = 30)
{
connect(remoteAddress = null, // required!
2022-12-17 22:24:33 +01:00
remoteAddressString = IPC_NAME,
remoteAddressPrettyString = IPC_NAME,
ipcId = ipcId,
connectionTimeoutSec = connectionTimeoutSec)
}
/**
* Will attempt to connect to the server, with a default 30 second connection timeout and will block until completed.
*
* Default connection is to localhost
*
* ### For a network address, it can be:
2022-03-15 20:43:43 +01:00
* - a network name ("localhost", "bob.example.org")
* - an IP address ("127.0.0.1", "123.123.123.123", "::1")
2020-09-09 01:33:09 +02:00
* - an InetAddress address
2022-03-15 20:43:43 +01:00
* - if no address is specified, and IPC is disabled in the config, then localhost will be selected
*
* ### For the IPC (Inter-Process-Communication) it must be:
* - `connect()` (only if ipc is enabled in the configuration)
* - `connect("")` (only if ipc is enabled in the configuration)
* - `connectIpc()`
*
* ### Case does not matter, and "localhost" is the default.
*
* @param remoteAddress The network host or ip address
* @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely
* @param reliable true if we want to create a reliable connection (for UDP connections, is message loss acceptable?).
*
* @throws IllegalArgumentException if the remote address is invalid
* @throws ClientTimedOutException if the client is unable to connect in x amount of time
* @throws ClientRejectedException if the client connection is rejected
*/
2022-07-15 06:58:49 +02:00
fun connect(
remoteAddress: String = "",
connectionTimeoutSec: Int = 30,
reliable: Boolean = true)
{
fun connect(dnsResolveType: ResolvedAddressTypes) {
val ipv4Requested = dnsResolveType == ResolvedAddressTypes.IPV4_ONLY || dnsResolveType == ResolvedAddressTypes.IPV4_PREFERRED
val inetAddress = formatCommonAddress(remoteAddress, ipv4Requested) {
// we already checked first if it's a valid IP address. This is called if it's not, since it might be a DNS lookup
val client = DnsClient()
client.resolvedAddressTypes(dnsResolveType)
val records = client.resolve(remoteAddress)
client.stop()
records?.get(0)
} ?: throw IllegalArgumentException("The remote address '$remoteAddress' cannot be found.")
2022-12-17 22:24:33 +01:00
val remoteAddressAsIp = IP.toString(inetAddress)
val formattedString = if (remoteAddress == remoteAddressAsIp) {
remoteAddress
} else {
"$remoteAddress ($remoteAddressAsIp)"
}
2022-08-18 12:38:44 +02:00
connect(remoteAddress = inetAddress,
// we check again, because the inetAddress that comes back from DNS, might not be what we expect
2022-12-17 22:24:33 +01:00
remoteAddressString = remoteAddressAsIp,
remoteAddressPrettyString = formattedString,
connectionTimeoutSec = connectionTimeoutSec,
reliable = reliable)
}
2020-09-09 01:33:09 +02:00
when {
// this is default IPC settings
remoteAddress.isEmpty() && config.enableIpc -> {
connectIpc(connectionTimeoutSec = connectionTimeoutSec)
}
// IPv6 takes precedence ONLY if it's enabled manually
config.enableIPv6 -> connect(ResolvedAddressTypes.IPV6_ONLY)
config.enableIPv4 -> connect(ResolvedAddressTypes.IPV4_ONLY)
IPv4.isPreferred -> connect(ResolvedAddressTypes.IPV4_PREFERRED)
IPv6.isPreferred -> connect(ResolvedAddressTypes.IPV6_PREFERRED)
else -> connect(ResolvedAddressTypes.IPV4_PREFERRED)
}
2020-09-09 01:33:09 +02:00
}
/**
* Will attempt to connect to the server, with a default 30 second connection timeout and will block until completed.
* If unable to connect within the specified timeout an exception will be thrown
2020-09-09 01:33:09 +02:00
*
* Default connection is to localhost
*
* ### For a network address, it can be:
2022-03-15 20:43:43 +01:00
* - a network name ("localhost", "bob.example.org")
2020-09-09 01:33:09 +02:00
* - an IP address ("127.0.0.1", "123.123.123.123", "::1")
* - an InetAddress address
2020-09-09 01:33:09 +02:00
*
* ### For the IPC (Inter-Process-Communication) it must be:
* - `connect()`
* - `connect("")`
* - `connectIpc()`
*
* ### Case does not matter, and "localhost" is the default.
2020-09-09 01:33:09 +02:00
*
* @param remoteAddress The network or if localhost, IPC address for the client to connect to
* @param ipcId The IPC publication address for the client to connect to
* @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely.
* @param reliable true if we want to create a reliable connection (for UDP connections, is message loss acceptable?).
2020-09-09 01:33:09 +02:00
*
* @throws IllegalArgumentException if the remote address is invalid
* @throws ClientTimedOutException if the client is unable to connect in x amount of time
* @throws ClientRejectedException if the client connection is rejected
* @throws ClientShutdownException if the client connection is shutdown while trying to connect
* @throws ClientException if there are misc errors
2020-09-09 01:33:09 +02:00
*/
@Suppress("DuplicatedCode")
private fun connect(
2022-07-15 06:58:49 +02:00
remoteAddress: InetAddress? = null,
remoteAddressString: String,
2022-12-17 22:24:33 +01:00
remoteAddressPrettyString: String,
2022-07-15 06:58:49 +02:00
// Default IPC ports are flipped because they are in the perspective of the SERVER
ipcId: Int = AeronDriver.IPC_HANDSHAKE_STREAM_ID,
2022-07-15 06:58:49 +02:00
connectionTimeoutSec: Int = 30,
reliable: Boolean = true)
{
// NOTE: it is critical to remember that Aeron DOES NOT like running from coroutines!
config as ClientConfiguration
2022-07-15 06:58:49 +02:00
2022-03-15 10:32:06 +01:00
require(connectionTimeoutSec >= 0) { "connectionTimeoutSec '$connectionTimeoutSec' is invalid. It must be >=0" }
if (isConnected) {
logger.error { "Unable to connect when already connected!" }
return
}
connection0 = null
// localhost/loopback IP might not always be 127.0.0.1 or ::1
2022-12-17 22:24:33 +01:00
// will be null if it's IPC
this.remoteAddress = remoteAddress
2022-12-17 22:24:33 +01:00
// will be exactly 'IPC' if it's IPC
// if it's an IP address, it will be the IP address
// if it's a DNS name, the name will be resolved, and it will be DNS (IP)
this.remoteAddressString = remoteAddressString
// we are done with initial configuration, now initialize aeron and the general state of this endpoint
try {
startDriver()
} catch (e: Exception) {
logger.error(e) { "Unable to start the network driver" }
return
}
// only try to connect via IPv4 if we have a network interface that supports it!
if (remoteAddress is Inet4Address && !IPv4.isAvailable) {
2022-12-17 22:24:33 +01:00
require(false) { "Unable to connect to the IPv4 address $remoteAddressPrettyString, there are no IPv4 interfaces available!" }
}
// only try to connect via IPv6 if we have a network interface that supports it!
if (remoteAddress is Inet6Address && !IPv6.isAvailable) {
2022-12-17 22:24:33 +01:00
require(false) { "Unable to connect to the IPv6 address $remoteAddressPrettyString, there are no IPv6 interfaces available!" }
}
if (remoteAddress != null && remoteAddress.isAnyLocalAddress) {
2022-12-17 22:24:33 +01:00
require(false) { "Cannot connect to $remoteAddressPrettyString It is an invalid address!" }
}
// IPC can be enabled TWO ways!
// - config.enableIpc
// - NULL remoteAddress
// It is entirely possible that the server does not have IPC enabled!
val autoChangeToIpc =
(config.enableIpc && (remoteAddress == null || remoteAddress.isLoopbackAddress)) || (!config.enableIpc && remoteAddress == null)
val handshake = ClientHandshake(crypto, this, logger)
var handshakeTimeoutSec = 5
var timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong())
if (DEBUG_CONNECTIONS) {
// connections are extremely difficult to diagnose when the connection timeout is short
timoutInNanos += TimeUnit.HOURS.toNanos(1).toInt()
handshakeTimeoutSec += TimeUnit.HOURS.toSeconds(1).toInt()
}
2022-07-15 06:58:49 +02:00
val startTime = System.nanoTime()
var success = false
while (timoutInNanos == 0L || System.nanoTime() - startTime < timoutInNanos) {
if (isShutdown()) {
// If we are connecting indefinitely, we have to make sure to end the connection process
val exception = ClientShutdownException("Unable to connect while shutting down")
logger.error(exception) { "Aborting connection retry attempt to server." }
listenerManager.notifyError(exception)
throw exception
}
// we have to pre-set the type (which will ultimately get set to the correct type on success)
var type = ""
2022-07-15 06:58:49 +02:00
try {
// the handshake connection is closed when the handshake has an error, or it is finished
2022-07-15 06:58:49 +02:00
val handshakeConnection = if (autoChangeToIpc) {
if (remoteAddress == null) {
logger.info { "IPC enabled" }
} else {
logger.warn { "IPC for loopback enabled and aeron is already running. Auto-changing network connection from " +
"'$remoteAddressString' -> IPC" }
}
// MAYBE the server doesn't have IPC enabled? If no, we need to connect via network instead
val ipcConnection = ClientIpcDriver(
streamId = ipcId,
sessionId = crypto.secureRandom.nextInt() + 1, // this helps prevent handshake collisions
remoteSessionId = AeronDriver.IPC_HANDSHAKE_SESSION_ID
2022-07-15 06:58:49 +02:00
)
2022-12-17 22:24:33 +01:00
type = "${ipcConnection.type} '$ipcId'"
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports
try {
ipcConnection.build(aeronDriver, logger)
ipcConnection
} catch (e: Exception) {
if (remoteAddress == null) {
// if we specified that we MUST use IPC, then we have to throw the exception, because there is no IPC
val clientException = ClientException("Unable to connect via IPC to server. No address specified so fallback is unavailable", e)
ListenerManager.cleanStackTraceInternal(clientException)
throw clientException
}
logger.info { "IPC for loopback enabled, but unable to connect. Retrying with address $remoteAddressString" }
// try a UDP connection instead
val udpConnection = ClientUdpDriver(
address = remoteAddress,
addressString = remoteAddressString,
port = config.port,
streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID,
sessionId = crypto.secureRandom.nextInt() + 1, // this helps prevent handshake collisions
connectionTimeoutSec = handshakeTimeoutSec,
isReliable = reliable
)
2022-12-17 22:24:33 +01:00
type = "${udpConnection.type} '$remoteAddressPrettyString:${config.port}'"
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports
udpConnection.build(aeronDriver, logger)
udpConnection
}
2022-07-15 06:58:49 +02:00
} else {
val udpConnection = ClientUdpDriver(
address = remoteAddress!!,
addressString = remoteAddressString,
port = config.port,
streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID,
sessionId = crypto.secureRandom.nextInt() + 1, // this helps prevent handshake collisions
connectionTimeoutSec = handshakeTimeoutSec,
isReliable = reliable
)
type = "${udpConnection.type} '$remoteAddressPrettyString:${config.port}'"
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports
udpConnection.build(aeronDriver, logger)
udpConnection
2022-07-15 06:58:49 +02:00
}
logger.info { handshakeConnection }
connect0(handshake, handshakeConnection, handshakeTimeoutSec)
2022-07-15 06:58:49 +02:00
success = true
2022-07-15 06:58:49 +02:00
// once we're done with the connection process, stop trying
break
} catch (e: ClientRetryException) {
handshake.reset()
// maybe the aeron driver isn't running? (or isn't running correctly?)
aeronDriver.closeIfSingle() // if we are the ONLY instance using the media driver, restart it
2022-07-15 06:58:49 +02:00
aeronDriver.start()
2022-07-15 06:58:49 +02:00
// short delay, since it failed we want to limit the retry rate to something slower than "as fast as the CPU can do it"
// we also want to go at SLIGHTLY slower that the aeron driver timeout frequency, this way - if there are connection or handshake issues, the server has the chance to expire the connections.
// If we go TOO FAST, then the server will EVENTUALLY have aeron errors (since it can't keep up per client). We literally
// want to have 1 in-flight handshake, per connection attempt, during the aeron connection timeout
// ALSO, we want to make sure we DO NOT approach the linger timeout!
sleep(aeronDriver.driverTimeout().coerceAtLeast(TimeUnit.NANOSECONDS.toSeconds(aeronDriver.getLingerNs()*2)))
2022-07-15 06:58:49 +02:00
if (logger.isTraceEnabled) {
logger.trace(e) { "Unable to connect to $type, retrying..." }
2022-07-15 06:58:49 +02:00
} else {
logger.info { "Unable to connect to $type, retrying..." }
}
} catch (e: ClientRejectedException) {
aeronDriver.closeIfSingle() // if we are the ONLY instance using the media driver, restart it
// short delay, since it failed we want to limit the retry rate to something slower than "as fast as the CPU can do it"
// we also want to go at SLIGHTLY slower that the aeron driver timeout frequency, this way - if there are connection or handshake issues, the server has the chance to expire the connections.
// If we go TOO FAST, then the server will EVENTUALLY have aeron errors (since it can't keep up per client). We literally
// want to have 1 in-flight handshake, per connection attempt, during the aeron connection timeout
// ALSO, we want to make sure we DO NOT approach the linger timeout!
2023-02-10 21:20:53 +01:00
sleep(aeronDriver.driverTimeout().coerceAtLeast(TimeUnit.NANOSECONDS.toSeconds(aeronDriver.getLingerNs() * 2)))
if (e.cause is ServerException) {
2022-11-12 00:38:23 +01:00
val cause = e.cause!!
val wrapped = ClientException(cause.message!!)
listenerManager.notifyError(wrapped)
throw wrapped
} else {
listenerManager.notifyError(e)
throw e
}
2022-07-15 06:58:49 +02:00
} catch (e: Exception) {
logger.error(e) { "[${handshake.connectKey}] : Un-recoverable error during handshake with $type. Aborting." }
aeronDriver.closeIfSingle() // if we are the ONLY instance using the media driver, restart it
// short delay, since it failed we want to limit the retry rate to something slower than "as fast as the CPU can do it"
// we also want to go at SLIGHTLY slower that the aeron driver timeout frequency, this way - if there are connection or handshake issues, the server has the chance to expire the connections.
// If we go TOO FAST, then the server will EVENTUALLY have aeron errors (since it can't keep up per client). We literally
// want to have 1 in-flight handshake, per connection attempt, during the aeron connection timeout
// ALSO, we want to make sure we DO NOT approach the linger timeout!
2023-02-10 21:20:53 +01:00
sleep(aeronDriver.driverTimeout().coerceAtLeast(TimeUnit.NANOSECONDS.toSeconds(aeronDriver.getLingerNs() * 2)))
2022-07-15 06:58:49 +02:00
listenerManager.notifyError(e)
throw e
}
}
2022-07-14 04:54:22 +02:00
2022-07-15 06:58:49 +02:00
if (!success) {
if (System.nanoTime() - startTime < timoutInNanos) {
// we timed out. Throw the appropriate exception
val exception = ClientTimedOutException("Unable to connect to the server at $type in $connectionTimeoutSec seconds")
2022-07-15 06:58:49 +02:00
logger.error(exception) { "Aborting connection attempt to server." }
listenerManager.notifyError(exception)
throw exception
}
2022-07-15 06:58:49 +02:00
// If we did not connect - throw an error. When `client.connect()` is called, either it connects or throws an error
val exception = ClientRejectedException("The server did not respond or permit the connection attempt")
ListenerManager.cleanStackTrace(exception)
logger.error(exception) { "Aborting connection retry attempt to server." }
listenerManager.notifyError(exception)
throw exception
}
}
// the handshake process might have to restart this connection process.
private fun connect0(handshake: ClientHandshake<CONNECTION>, handshakeConnection: MediaDriverClient, connectionTimeoutSec: Int) {
// this will block until the connection timeout, and throw an exception if we were unable to connect with the server
val isUsingIPC = handshakeConnection is ClientIpcDriver
// throws(ConnectTimedOutException::class, ClientRejectedException::class, ClientException::class)
val connectionInfo = handshake.hello(handshakeConnection, connectionTimeoutSec)
// VALIDATE:: check to see if the remote connection's public key has changed!
val validateRemoteAddress = if (isUsingIPC) {
PublicKeyValidationState.VALID
} else {
crypto.validateRemoteAddress(remoteAddress!!, remoteAddressString, connectionInfo.publicKey)
}
if (validateRemoteAddress == PublicKeyValidationState.INVALID) {
handshakeConnection.subscription.close()
handshakeConnection.publication.close()
val exception = ClientRejectedException("Connection to $remoteAddressString not allowed! Public key mismatch.")
logger.error(exception) { "Validation error" }
throw exception
}
// VALIDATE:: If the serialization DOES NOT match between the client/server, then the server will emit a log, and the
// client will timeout. SPECIFICALLY.... we do not give class serialization/registration info to the client - in case the client
// is rogue, we do not want to carelessly provide info.
///////////////
//// RMI
///////////////
// we set up our kryo information once we connect to a server (using the server's kryo registration details)
val kryoConfiguredFromServer = serialization.finishClientConnect(connectionInfo.kryoRegistrationDetails)
if (kryoConfiguredFromServer == null) {
handshakeConnection.subscription.close()
handshakeConnection.publication.close()
// because we are getting the class registration details from the SERVER, this should never be the case.
// It is still and edge case where the reconstruction of the registration details fails (maybe because of custom serializers)
val exception = if (isUsingIPC) {
ClientRejectedException("[${handshake.connectKey}] Connection to IPC has incorrect class registration details!!")
} else {
ClientRejectedException("[${handshake.connectKey}] Connection to $remoteAddressString has incorrect class registration details!!")
}
ListenerManager.cleanStackTraceInternal(exception)
throw exception
}
2023-02-10 21:20:53 +01:00
// every time we connect to a server, we have to reconfigure AND reassign the readKryos.
readKryo = kryoConfiguredFromServer
streamingReadKryo = serialization.initKryo()
///////////////
//// CONFIG THE CLIENT
///////////////
// we are now connected, so we can connect to the NEW client-specific ports
2021-04-27 10:28:36 +02:00
val clientConnection = if (isUsingIPC) {
// Create a subscription at the given address and port, using the given stream ID.
val driver = ClientIpcDriver(
streamId = connectionInfo.streamId,
sessionId = connectionInfo.sessionId,
remoteSessionId = connectionInfo.port
)
driver.build(aeronDriver, logger)
logger.info { "Creating new IPC connection to $driver" }
MediaDriverConnectInfo(
subscription = driver.subscription,
publication = driver.publication,
subscriptionPort = connectionInfo.sessionId,
publicationPort = driver.streamId,
streamId = 0, // this is because with IPC, we have stream sub/pub (which are replaced as port sub/pub)
sessionId = driver.sessionId,
isReliable = driver.isReliable,
remoteAddress = null,
remoteAddressString = "ipc"
)
}
else {
val driver = ClientUdpDriver(
address = (handshakeConnection as ClientUdpDriver).address,
addressString = handshakeConnection.addressString,
port = connectionInfo.port, // this is the port that we connect to
2021-04-27 10:28:36 +02:00
streamId = connectionInfo.streamId,
sessionId = connectionInfo.sessionId,
connectionTimeoutSec = connectionTimeoutSec,
2021-04-27 10:28:36 +02:00
isReliable = handshakeConnection.isReliable)
// we have to construct how the connection will communicate!
// we don't care about the subscription, only the publication
driver.build(aeronDriver, logger)
logger.info { "Creating new connection to $driver" }
MediaDriverConnectInfo(
subscription = driver.subscription,
publication = driver.publication,
subscriptionPort = driver.subscriptionPort,
publicationPort = driver.port,
streamId = driver.streamId,
sessionId = driver.sessionId,
isReliable = driver.isReliable,
remoteAddress = driver.address,
remoteAddressString = IP.toString(driver.address)
)
}
val sessionId = clientConnection.sessionId
val streamId = clientConnection.streamId
val aeronLogInfo = "$sessionId/$streamId"
// have to rebuild the client pub/sub for the next part of the handshake (since it's a 1-shot deal for the server per session)
handshakeConnection.subscription.close()
if (handshakeConnection is ClientUdpDriver) {
handshakeConnection.publication.close()
}
handshakeConnection.sessionId = crypto.secureRandom.nextInt() + 1 // this helps prevent handshake collisions
handshakeConnection.build(aeronDriver, logger)
2020-09-19 22:06:54 +02:00
val newConnection: CONNECTION
if (isUsingIPC) {
newConnection = connectionFunc(ConnectionParams(this, clientConnection, PublicKeyValidationState.VALID))
} else {
newConnection = connectionFunc(ConnectionParams(this, clientConnection, validateRemoteAddress))
2020-09-19 22:06:54 +02:00
remoteAddress!!
// VALIDATE are we allowed to connect to this server (now that we have the initial server information)
val permitConnection = listenerManager.notifyFilter(newConnection)
if (!permitConnection) {
handshakeConnection.subscription.close()
handshakeConnection.publication.close()
val exception = ClientRejectedException("[$aeronLogInfo - ${handshake.connectKey}] Connection (${newConnection.id}) to $remoteAddressString was not permitted!")
ListenerManager.cleanStackTrace(exception)
logger.error(exception) { "Permission error" }
2020-09-19 22:06:54 +02:00
throw exception
}
logger.info { "[$aeronLogInfo - ${handshake.connectKey}] Connection (${newConnection.id}) adding new signature for $remoteAddressString : ${connectionInfo.publicKey.toHexString()}" }
storage.addRegisteredServerKey(remoteAddress!!, connectionInfo.publicKey)
}
2020-09-19 22:06:54 +02:00
//////////////
/// Extra Close action
//////////////
2022-08-02 21:14:54 +02:00
newConnection.closeAction = {
// this is called whenever connection.close() is called by the framework or via client.close()
// on the client, we want to GUARANTEE that the disconnect happens-before connect.
2022-06-15 23:53:26 +02:00
if (!lockStepForConnect.compareAndSet(null, Mutex(locked = true))) {
logger.error { "[$aeronLogInfo - ${handshake.connectKey}] Connection ${newConnection.id} : close lockStep for disconnect was in the wrong state!" }
}
2022-06-06 17:18:46 +02:00
isConnected = false
// this is called whenever connection.close() is called by the framework or via client.close()
// make sure to call our client.notifyDisconnect() callbacks
// this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback
2022-07-14 04:54:22 +02:00
actionDispatch.launch {
2022-08-02 21:14:54 +02:00
listenerManager.notifyDisconnect(connection)
2022-06-15 23:53:26 +02:00
lockStepForConnect.getAndSet(null)?.unlock()
}
}
// before we finish creating the connection, we initialize it (in case there needs to be logic that happens-before `onConnect` calls occur
listenerManager.notifyInit(newConnection)
connection0 = newConnection
addConnection(newConnection)
// tell the server our connection handshake is done, and the connection can now listen for data.
// also closes the handshake (will also throw connect timeout exception)
// this value matches the server, and allows for a more robust connection attempt
val successAttemptTimeout = config.connectionCloseTimeoutInSeconds * 2
try {
handshake.done(handshakeConnection, successAttemptTimeout)
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo - ${handshake.connectKey}] Connection (${newConnection.id}) to $remoteAddressString error during handshake" }
throw e
}
// finished with the handshake, so always close the connection publication
// The subscription is RE-USED, so we don't close that!
handshakeConnection.publication.close()
isConnected = true
logger.debug { "[$aeronLogInfo - ${handshake.connectKey}] Connection (${newConnection.id}) to $remoteAddressString done with handshake." }
// this forces the current thread to WAIT until the network poll system has started
val pollStartupLatch = CountDownLatch(1)
// have to make a new thread to listen for incoming data!
// SUBSCRIPTIONS ARE NOT THREAD SAFE! Only one thread at a time can poll them
val networkEventProcessor = Runnable {
pollStartupLatch.countDown()
val pollIdleStrategy = config.pollIdleStrategy.cloneToNormal()
while (!isShutdown()) {
2022-07-03 13:13:38 +02:00
if (!newConnection.isClosedViaAeron()) {
// Polls the AERON media driver subscription channel for incoming messages
val pollCount = newConnection.poll()
2022-07-03 13:13:38 +02:00
// 0 means we idle. >0 means reset and don't idle (because there are likely more poll events)
pollIdleStrategy.idle(pollCount)
} else {
// If the connection has either been closed, or has expired, it needs to be cleaned-up/deleted.
logger.debug { "[$aeronLogInfo] connection expired" }
2022-08-02 21:15:45 +02:00
// NOTE: We do not shutdown the client!! The client is only closed by explicitly calling `client.close()`
newConnection.close()
return@Runnable
}
}
}
config.networkInterfaceEventDispatcher.submit(networkEventProcessor)
pollStartupLatch.await()
// these have to be in two SEPARATE "runnables" otherwise...
// if something inside-of listenerManager.notifyConnect is blocking or suspends, then polling will never happen!
actionDispatch.launch {
lockStepForConnect.getAndSet(null)?.withLock { }
listenerManager.notifyConnect(newConnection)
}
}
/**
* true if the remote public key changed. This can be useful if specific actions are necessary when the key has changed.
*/
val remoteKeyHasChanged: Boolean
get() = connection.hasRemoteKeyChanged()
/**
* true if this connection is an IPC connection
*/
val isIPC: Boolean
get() = connection.isIpc
/**
* @return true if this connection is a network connection
*/
val isNetwork: Boolean
get() = connection.isNetwork
/**
* @return the connection (TCP or IPC) id of this connection.
*/
val id: Int
get() = connection.id
/**
* the connection used by the client, this is only valid after the client has connected
*/
val connection: CONNECTION
get() = connection0 as CONNECTION
/**
* Sends a message to the server, if the connection is closed for any reason, this returns false.
*
* @return true if the message was sent successfully, false if the connection has been closed
*/
2022-08-18 22:01:42 +02:00
suspend fun send(message: Any): Boolean {
val c = connection0
return if (c != null) {
c.send(message)
} else {
val exception = ClientException("Cannot send a message when there is no connection!")
logger.error(exception) { "No connection!" }
false
}
}
2022-08-18 22:01:42 +02:00
/**
* Sends a message to the server, if the connection is closed for any reason, this returns false.
*
* @return true if the message was sent successfully, false if the connection has been closed
*/
fun sendBlocking(message: Any): Boolean {
val c = connection0
return if (c != null) {
runBlocking {
c.send(message)
}
} else {
val exception = ClientException("Cannot send a message when there is no connection!")
logger.error(exception) { "No connection!" }
false
}
}
2021-04-29 10:25:25 +02:00
/**
* Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection.
*
2021-04-29 10:25:25 +02:00
* @param function called when the ping returns (ie: update time/latency counters/metrics/etc)
*
* @return true if the ping was successfully sent to the client
*/
suspend fun ping(pingTimeoutSeconds: Int = PingManager.DEFAULT_TIMEOUT_SECONDS, function: suspend Ping.() -> Unit): Boolean {
val c = connection0
2021-04-29 10:25:25 +02:00
if (c != null) {
return pingManager.ping(c, pingTimeoutSeconds, actionDispatch, responseManager, logger, function)
} else {
logger.error(ClientException("Cannot send a ping when there is no connection!")) { "No connection!" }
}
return false
}
/**
2021-04-29 10:25:25 +02:00
* Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection.
*
* @param function called when the ping returns (ie: update time/latency counters/metrics/etc)
*/
fun pingBlocking(pingTimeoutSeconds: Int = PingManager.DEFAULT_TIMEOUT_SECONDS, function: suspend Ping.() -> Unit): Boolean {
return runBlocking {
ping(pingTimeoutSeconds, function)
2021-04-29 10:25:25 +02:00
}
}
/**
* Removes the specified host address from the list of registered server keys.
*/
2020-09-09 01:33:09 +02:00
fun removeRegisteredServerKey(address: InetAddress) {
val savedPublicKey = storage.getRegisteredServerKey(address)
if (savedPublicKey != null) {
2020-09-09 01:33:09 +02:00
logger.debug { "Deleting remote IP address key $address" }
storage.removeRegisteredServerKey(address)
}
}
final override fun close0() {
2022-07-15 06:58:49 +02:00
// no impl
}
}