2020-08-12 23:30:16 +02:00
|
|
|
/*
|
2020-08-19 15:29:35 +02:00
|
|
|
* Copyright 2020 dorkbox, llc
|
2020-08-12 23:30:16 +02:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
|
2021-08-23 08:48:05 +02:00
|
|
|
import dorkbox.bytes.toHexString
|
2022-06-04 01:32:58 +02:00
|
|
|
import dorkbox.dns.DnsClient
|
2022-03-08 08:41:06 +01:00
|
|
|
import dorkbox.netUtil.IP
|
|
|
|
import dorkbox.netUtil.IPv4
|
|
|
|
import dorkbox.netUtil.IPv6
|
|
|
|
import dorkbox.netUtil.Inet4
|
|
|
|
import dorkbox.netUtil.Inet6
|
2022-06-04 01:32:58 +02:00
|
|
|
import dorkbox.netUtil.dnsUtils.ResolvedAddressTypes
|
2021-04-27 13:57:47 +02:00
|
|
|
import dorkbox.network.aeron.AeronDriver
|
2020-09-09 01:33:09 +02:00
|
|
|
import dorkbox.network.aeron.IpcMediaDriverConnection
|
2022-03-24 00:32:58 +01:00
|
|
|
import dorkbox.network.aeron.MediaDriverConnection
|
2021-04-27 10:28:36 +02:00
|
|
|
import dorkbox.network.aeron.UdpMediaDriverClientConnection
|
2022-03-08 08:41:06 +01:00
|
|
|
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
|
|
|
|
import dorkbox.network.connection.eventLoop
|
2020-09-22 19:41:19 +02:00
|
|
|
import dorkbox.network.coroutines.SuspendWaiter
|
2020-09-09 01:33:09 +02:00
|
|
|
import dorkbox.network.exceptions.ClientException
|
|
|
|
import dorkbox.network.exceptions.ClientRejectedException
|
2022-04-04 16:30:05 +02:00
|
|
|
import dorkbox.network.exceptions.ClientRetryException
|
2020-09-09 01:33:09 +02:00
|
|
|
import dorkbox.network.exceptions.ClientTimedOutException
|
2020-08-12 23:30:16 +02:00
|
|
|
import dorkbox.network.handshake.ClientHandshake
|
2021-04-30 16:01:25 +02:00
|
|
|
import dorkbox.network.ping.Ping
|
2021-07-08 14:33:24 +02:00
|
|
|
import dorkbox.network.ping.PingManager
|
2020-09-02 02:39:05 +02:00
|
|
|
import kotlinx.atomicfu.atomic
|
2022-03-24 00:32:58 +01:00
|
|
|
import kotlinx.coroutines.delay
|
2020-08-12 23:30:16 +02:00
|
|
|
import kotlinx.coroutines.launch
|
2021-04-30 16:01:25 +02:00
|
|
|
import kotlinx.coroutines.runBlocking
|
2020-09-09 01:33:09 +02:00
|
|
|
import java.net.Inet4Address
|
2020-09-10 00:35:01 +02:00
|
|
|
import java.net.Inet6Address
|
2020-09-09 01:33:09 +02:00
|
|
|
import java.net.InetAddress
|
2022-03-24 00:32:58 +01:00
|
|
|
import java.util.concurrent.*
|
2020-08-12 23:30:16 +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.
|
2021-07-09 15:16:12 +02:00
|
|
|
*
|
|
|
|
* @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)
|
2020-08-12 23:30:16 +02:00
|
|
|
*/
|
2022-03-24 00:32:58 +01:00
|
|
|
@Suppress("unused")
|
2021-07-09 15:16:12 +02:00
|
|
|
open class Client<CONNECTION : Connection>(
|
2022-05-28 12:15:45 +02:00
|
|
|
config: Configuration = Configuration(),
|
|
|
|
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 connectionFunc allows for custom connection implementations defined as a unit function
|
|
|
|
*/
|
|
|
|
constructor(config: Configuration,
|
|
|
|
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: Configuration,
|
|
|
|
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: Configuration)
|
|
|
|
: this(config,
|
|
|
|
{
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
Connection(it) as CONNECTION
|
|
|
|
},
|
|
|
|
Client::class.java.simpleName)
|
|
|
|
|
2021-07-09 15:16:12 +02:00
|
|
|
|
|
|
|
|
2020-08-12 23:30:16 +02:00
|
|
|
companion object {
|
|
|
|
/**
|
|
|
|
* Gets the version number.
|
|
|
|
*/
|
2022-06-08 00:28:02 +02:00
|
|
|
const val version = "5.15"
|
2021-04-09 20:24:45 +02:00
|
|
|
|
2021-04-30 21:18:57 +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
|
|
|
}
|
2020-08-12 23:30:16 +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.
|
|
|
|
*/
|
2020-09-09 01:33:09 +02:00
|
|
|
private var remoteAddress0: InetAddress? = IPv4.LOCALHOST
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-08-15 13:21:20 +02:00
|
|
|
@Volatile
|
|
|
|
private var isConnected = false
|
2020-08-12 23:30:16 +02:00
|
|
|
|
|
|
|
// is valid when there is a connection to the server, otherwise it is null
|
2020-09-03 01:31:08 +02:00
|
|
|
private var connection0: CONNECTION? = null
|
2020-08-12 23:30:16 +02:00
|
|
|
|
|
|
|
|
2021-04-29 02:10:34 +02:00
|
|
|
// 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'.
|
2021-04-30 16:01:25 +02:00
|
|
|
private val lockStepForConnect = atomic<SuspendWaiter?>(null)
|
2020-09-02 02:39:05 +02:00
|
|
|
|
2020-09-25 14:49:17 +02:00
|
|
|
final override fun newException(message: String, cause: Throwable?): Throwable {
|
2020-08-12 23:30:16 +02:00
|
|
|
return ClientException(message, cause)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-08-15 13:21:20 +02:00
|
|
|
* Will attempt to connect to the server, with a default 30 second connection timeout and will block until completed.
|
2020-08-12 23:30:16 +02:00
|
|
|
*
|
2020-08-15 13:21:20 +02:00
|
|
|
* Default connection is to localhost
|
2020-08-12 23:30:16 +02:00
|
|
|
*
|
2020-08-15 13:21:20 +02:00
|
|
|
* ### For a network address, it can be:
|
2022-03-15 20:43:43 +01:00
|
|
|
* - a network name ("localhost", "bob.example.org")
|
2020-08-15 13:21:20 +02:00
|
|
|
* - 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
|
2020-08-12 23:30:16 +02:00
|
|
|
*
|
2020-09-02 02:39:05 +02:00
|
|
|
* ### For the IPC (Inter-Process-Communication) it must be:
|
2021-07-06 15:38:53 +02:00
|
|
|
* - `connect()` (only if ipc is enabled in the configuration)
|
|
|
|
* - `connect("")` (only if ipc is enabled in the configuration)
|
2021-04-30 16:01:25 +02:00
|
|
|
* - `connectIpc()`
|
2020-08-12 23:30:16 +02:00
|
|
|
*
|
2021-04-30 16:01:25 +02:00
|
|
|
* ### Case does not matter, and "localhost" is the default.
|
2020-08-12 23:30:16 +02:00
|
|
|
*
|
2021-04-30 16:01:25 +02:00
|
|
|
* @param remoteAddress The network host or ip address
|
2022-03-08 08:41:06 +01:00
|
|
|
* @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely
|
2021-04-30 16:01:25 +02:00
|
|
|
* @param reliable true if we want to create a reliable connection (for UDP connections, is message loss acceptable?).
|
2020-08-12 23:30:16 +02:00
|
|
|
*
|
2020-08-15 13:21:20 +02:00
|
|
|
* @throws IllegalArgumentException if the remote address is invalid
|
2020-08-12 23:30:16 +02:00
|
|
|
* @throws ClientTimedOutException if the client is unable to connect in x amount of time
|
2020-08-15 13:21:20 +02:00
|
|
|
* @throws ClientRejectedException if the client connection is rejected
|
2020-08-12 23:30:16 +02:00
|
|
|
*/
|
2020-09-23 17:08:25 +02:00
|
|
|
@Suppress("BlockingMethodInNonBlockingContext")
|
2021-04-30 16:01:25 +02:00
|
|
|
fun connect(remoteAddress: String = "",
|
2022-03-08 08:41:06 +01:00
|
|
|
connectionTimeoutSec: Int = 30,
|
2021-04-30 16:01:25 +02:00
|
|
|
reliable: Boolean = true) {
|
2020-09-09 01:33:09 +02:00
|
|
|
when {
|
|
|
|
// this is default IPC settings
|
2022-03-24 00:32:58 +01:00
|
|
|
remoteAddress.isEmpty() && config.enableIpc -> {
|
2022-04-04 23:04:27 +02:00
|
|
|
connectIpc(connectionTimeoutSec = connectionTimeoutSec)
|
2021-04-30 16:01:25 +02:00
|
|
|
}
|
2020-09-09 01:33:09 +02:00
|
|
|
|
2021-04-30 16:01:25 +02:00
|
|
|
IPv4.isPreferred -> {
|
2022-06-04 01:32:58 +02:00
|
|
|
// we have to check first if it's a valid IPv4 address. If not, maybe it's a DNS lookup
|
|
|
|
val inet4Address = if (IPv4.isValid(remoteAddress)) {
|
|
|
|
Inet4.toAddress(remoteAddress)
|
|
|
|
} else {
|
|
|
|
val client = DnsClient()
|
|
|
|
client.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY)
|
|
|
|
val records = client.resolve(remoteAddress)
|
|
|
|
client.stop()
|
2022-06-08 00:25:01 +02:00
|
|
|
records?.get(0)
|
2022-06-04 01:32:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (inet4Address == null) {
|
|
|
|
throw IllegalArgumentException("The remote address '$remoteAddress' cannot be found.")
|
|
|
|
}
|
|
|
|
|
|
|
|
connect(remoteAddress = inet4Address,
|
2022-03-24 00:32:58 +01:00
|
|
|
connectionTimeoutSec = connectionTimeoutSec,
|
|
|
|
reliable = reliable
|
2021-04-30 16:01:25 +02:00
|
|
|
)
|
|
|
|
}
|
2020-09-09 01:33:09 +02:00
|
|
|
|
2021-04-30 16:01:25 +02:00
|
|
|
IPv6.isPreferred -> {
|
2022-06-04 01:32:58 +02:00
|
|
|
// we have to check first if it's a valid IPv6 address. If not, maybe it's a DNS lookup
|
|
|
|
val inet6Address = if (IPv6.isValid(remoteAddress)) {
|
|
|
|
Inet6.toAddress(remoteAddress)
|
|
|
|
} else {
|
|
|
|
val client = DnsClient()
|
|
|
|
client.resolvedAddressTypes(ResolvedAddressTypes.IPV6_ONLY)
|
|
|
|
val records = client.resolve(remoteAddress)
|
|
|
|
client.stop()
|
2022-06-08 00:25:01 +02:00
|
|
|
records?.get(0)
|
2022-06-04 01:32:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (inet6Address == null) {
|
|
|
|
throw IllegalArgumentException("The remote address '$remoteAddress' cannot be found.")
|
|
|
|
}
|
|
|
|
|
2022-03-24 00:32:58 +01:00
|
|
|
connect(remoteAddress = Inet6.toAddress(remoteAddress),
|
|
|
|
connectionTimeoutSec = connectionTimeoutSec,
|
|
|
|
reliable = reliable
|
2021-04-30 16:01:25 +02:00
|
|
|
)
|
|
|
|
}
|
2020-09-22 19:39:42 +02:00
|
|
|
|
|
|
|
// if there is no preference, then try to connect via IPv4
|
2021-04-30 16:01:25 +02:00
|
|
|
else -> {
|
2022-06-04 01:32:58 +02:00
|
|
|
// we have to check first if it's a valid IPv4 address. If not, maybe it's a DNS lookup
|
|
|
|
val inetAddress = if (IP.isValid(remoteAddress)) {
|
|
|
|
IP.toAddress(remoteAddress)
|
|
|
|
} else {
|
|
|
|
val client = DnsClient()
|
|
|
|
client.resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED)
|
|
|
|
val records = client.resolve(remoteAddress)
|
|
|
|
client.stop()
|
2022-06-08 00:25:01 +02:00
|
|
|
records?.get(0)
|
2022-06-04 01:32:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (inetAddress == null) {
|
|
|
|
throw IllegalArgumentException("The remote address '$remoteAddress' cannot be found.")
|
|
|
|
}
|
|
|
|
|
|
|
|
connect(remoteAddress = inetAddress,
|
2022-03-24 00:32:58 +01:00
|
|
|
connectionTimeoutSec = connectionTimeoutSec,
|
|
|
|
reliable = reliable
|
2021-04-30 16:01:25 +02:00
|
|
|
)
|
|
|
|
}
|
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.
|
|
|
|
*
|
|
|
|
* Default connection is to localhost
|
|
|
|
*
|
|
|
|
* ### For a network address, it can be:
|
2022-03-24 00:32:58 +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
|
|
|
|
*
|
|
|
|
* ### For the IPC (Inter-Process-Communication) it must be:
|
|
|
|
* - `connect()`
|
|
|
|
* - `connect("")`
|
2021-04-30 16:01:25 +02:00
|
|
|
* - `connectIpc()`
|
2020-09-09 01:33:09 +02:00
|
|
|
*
|
2021-04-30 16:01:25 +02:00
|
|
|
* ### 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
|
2022-03-08 08:41:06 +01:00
|
|
|
* @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely
|
2021-04-30 16:01:25 +02:00
|
|
|
* @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
|
|
|
|
*/
|
2021-04-30 16:01:25 +02:00
|
|
|
fun connect(remoteAddress: InetAddress,
|
2022-03-08 08:41:06 +01:00
|
|
|
connectionTimeoutSec: Int = 30,
|
2021-04-30 16:01:25 +02:00
|
|
|
reliable: Boolean = true) {
|
|
|
|
|
2020-09-09 01:33:09 +02:00
|
|
|
// Default IPC ports are flipped because they are in the perspective of the SERVER
|
|
|
|
connect(remoteAddress = remoteAddress,
|
2021-04-27 13:57:47 +02:00
|
|
|
ipcPublicationId = AeronDriver.IPC_HANDSHAKE_STREAM_ID_SUB,
|
|
|
|
ipcSubscriptionId = AeronDriver.IPC_HANDSHAKE_STREAM_ID_PUB,
|
2022-03-08 08:41:06 +01:00
|
|
|
connectionTimeoutSec = connectionTimeoutSec,
|
2020-09-09 01:33:09 +02:00
|
|
|
reliable = reliable)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Will attempt to connect to the server via IPC, with a default 30 second connection timeout and will block until completed.
|
|
|
|
*
|
|
|
|
* @param ipcPublicationId The IPC publication address for the client to connect to
|
|
|
|
* @param ipcSubscriptionId The IPC subscription address for the client to connect to
|
2022-03-08 08:41:06 +01:00
|
|
|
* @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely.
|
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
|
|
|
|
*/
|
2020-08-25 17:45:08 +02:00
|
|
|
@Suppress("DuplicatedCode")
|
2021-04-30 16:01:25 +02:00
|
|
|
fun connectIpc(ipcPublicationId: Int = AeronDriver.IPC_HANDSHAKE_STREAM_ID_SUB,
|
|
|
|
ipcSubscriptionId: Int = AeronDriver.IPC_HANDSHAKE_STREAM_ID_PUB,
|
2022-03-08 08:41:06 +01:00
|
|
|
connectionTimeoutSec: Int = 30) {
|
2020-09-09 01:33:09 +02:00
|
|
|
// Default IPC ports are flipped because they are in the perspective of the SERVER
|
|
|
|
|
|
|
|
require(ipcPublicationId != ipcSubscriptionId) { "IPC publication and subscription ports cannot be the same! The must match the server's configuration." }
|
|
|
|
|
|
|
|
connect(remoteAddress = null, // required!
|
|
|
|
ipcPublicationId = ipcPublicationId,
|
|
|
|
ipcSubscriptionId = ipcSubscriptionId,
|
2022-03-08 08:41:06 +01:00
|
|
|
connectionTimeoutSec = connectionTimeoutSec)
|
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.
|
|
|
|
*
|
|
|
|
* 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")
|
2021-04-30 16:01:25 +02:00
|
|
|
* - an InetAddress address
|
2020-09-09 01:33:09 +02:00
|
|
|
*
|
|
|
|
* ### For the IPC (Inter-Process-Communication) it must be:
|
2021-04-30 16:01:25 +02:00
|
|
|
* - `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 ipcPublicationId The IPC publication address for the client to connect to
|
|
|
|
* @param ipcSubscriptionId The IPC subscription address for the client to connect to
|
2022-03-08 08:41:06 +01:00
|
|
|
* @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely.
|
2021-04-30 16:01:25 +02:00
|
|
|
* @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
|
2021-07-06 15:38:53 +02:00
|
|
|
* @throws ClientException if there are misc errors
|
2020-09-09 01:33:09 +02:00
|
|
|
*/
|
|
|
|
@Suppress("DuplicatedCode")
|
2021-04-30 16:01:25 +02:00
|
|
|
private fun connect(remoteAddress: InetAddress? = null,
|
|
|
|
// Default IPC ports are flipped because they are in the perspective of the SERVER
|
|
|
|
ipcPublicationId: Int = AeronDriver.IPC_HANDSHAKE_STREAM_ID_SUB,
|
|
|
|
ipcSubscriptionId: Int = AeronDriver.IPC_HANDSHAKE_STREAM_ID_PUB,
|
2022-03-08 08:41:06 +01:00
|
|
|
connectionTimeoutSec: Int = 30,
|
2021-04-30 16:01:25 +02:00
|
|
|
reliable: Boolean = true) {
|
2022-03-15 10:32:06 +01:00
|
|
|
require(connectionTimeoutSec >= 0) { "connectionTimeoutSec '$connectionTimeoutSec' is invalid. It must be >=0" }
|
2020-09-25 18:31:23 +02:00
|
|
|
|
2020-08-15 13:21:20 +02:00
|
|
|
if (isConnected) {
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error { "Unable to connect when already connected!" }
|
2020-08-12 23:30:16 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-09-11 01:14:22 +02:00
|
|
|
// localhost/loopback IP might not always be 127.0.0.1 or ::1
|
|
|
|
this.remoteAddress0 = remoteAddress
|
2020-09-03 01:31:08 +02:00
|
|
|
connection0 = null
|
2020-09-02 02:39:05 +02:00
|
|
|
|
2020-08-25 17:45:08 +02:00
|
|
|
// we are done with initial configuration, now initialize aeron and the general state of this endpoint
|
2021-07-06 15:38:53 +02:00
|
|
|
try {
|
2022-05-30 02:45:50 +02:00
|
|
|
initEndpointState()
|
2021-07-06 15:38:53 +02:00
|
|
|
} catch (e: Exception) {
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error(e) { "Unable to initialize the endpoint state" }
|
2021-07-06 15:38:53 +02:00
|
|
|
return
|
|
|
|
}
|
2020-08-25 17:45:08 +02:00
|
|
|
|
2020-09-10 00:35:01 +02:00
|
|
|
// only try to connect via IPv4 if we have a network interface that supports it!
|
|
|
|
if (remoteAddress is Inet4Address && !IPv4.isAvailable) {
|
2020-09-11 01:14:22 +02:00
|
|
|
require(false) { "Unable to connect to the IPv4 address ${IPv4.toString(remoteAddress)}, there are no IPv4 interfaces available!" }
|
2020-09-10 00:35:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// only try to connect via IPv6 if we have a network interface that supports it!
|
|
|
|
if (remoteAddress is Inet6Address && !IPv6.isAvailable) {
|
2020-09-11 01:14:22 +02:00
|
|
|
require(false) { "Unable to connect to the IPv6 address ${IPv6.toString(remoteAddress)}, there are no IPv6 interfaces available!" }
|
2020-09-10 00:35:01 +02:00
|
|
|
}
|
|
|
|
|
2020-09-11 01:14:22 +02:00
|
|
|
if (remoteAddress != null && remoteAddress.isAnyLocalAddress) {
|
|
|
|
require(false) { "Cannot connect to ${IP.toString(remoteAddress)} It is an invalid address!" }
|
2020-08-12 23:30:16 +02:00
|
|
|
}
|
|
|
|
|
2021-04-30 22:39:36 +02:00
|
|
|
// IPC can be enabled TWO ways!
|
|
|
|
// - config.enableIpc
|
|
|
|
// - NULL remoteAddress
|
|
|
|
// It is entirely possible that the server does not have IPC enabled!
|
2022-03-24 00:32:58 +01:00
|
|
|
val autoChangeToIpc =
|
|
|
|
(config.enableIpc && (remoteAddress == null || remoteAddress.isLoopbackAddress)) || (!config.enableIpc && remoteAddress == null)
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2021-07-06 15:38:53 +02:00
|
|
|
val handshake = ClientHandshake(crypto, this, logger)
|
2022-03-24 00:32:58 +01:00
|
|
|
|
|
|
|
runBlocking {
|
|
|
|
val handshakeTimeout = 5
|
|
|
|
val timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong())
|
|
|
|
val startTime = System.nanoTime()
|
|
|
|
while (timoutInNanos == 0L || System.nanoTime() - startTime < timoutInNanos) {
|
|
|
|
try {
|
|
|
|
val handshakeConnection = if (autoChangeToIpc) {
|
2022-04-04 23:04:27 +02:00
|
|
|
buildIpcHandshake(
|
|
|
|
ipcSubscriptionId = ipcSubscriptionId,
|
|
|
|
ipcPublicationId = ipcPublicationId,
|
|
|
|
connectionTimeoutSec = handshakeTimeout,
|
|
|
|
reliable = reliable
|
|
|
|
)
|
2022-03-24 00:32:58 +01:00
|
|
|
} else {
|
2022-04-04 23:04:27 +02:00
|
|
|
buildUdpHandshake(connectionTimeoutSec = handshakeTimeout, reliable = reliable)
|
2022-03-24 00:32:58 +01:00
|
|
|
}
|
|
|
|
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.info { handshakeConnection.clientInfo }
|
2022-03-24 00:32:58 +01:00
|
|
|
|
|
|
|
|
|
|
|
connect0(handshake, handshakeConnection, handshakeTimeout)
|
|
|
|
|
|
|
|
// once we're done with the connection process, stop trying
|
|
|
|
break
|
2022-04-04 16:30:05 +02:00
|
|
|
} catch (e: ClientRetryException) {
|
2022-03-24 00:32:58 +01:00
|
|
|
handshake.reset()
|
|
|
|
|
|
|
|
// short delay, since it failed we want to limit the retry rate to something slower than "as fast as the CPU can do it"
|
|
|
|
delay(500)
|
|
|
|
if (logger.isTraceEnabled) {
|
2022-06-01 23:51:45 +02:00
|
|
|
logger.trace(e) { "Unable to connect to ${IP.toString(remoteAddress!!)}, retrying..." }
|
2022-03-24 00:32:58 +01:00
|
|
|
} else {
|
2022-06-01 23:51:45 +02:00
|
|
|
logger.info { "Unable to connect to ${IP.toString(remoteAddress!!)}, retrying..." }
|
2022-03-24 00:32:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error(e) { "Un-recoverable error during handshake. Aborting." }
|
2022-04-04 16:30:05 +02:00
|
|
|
listenerManager.notifyError(e)
|
2022-03-24 00:32:58 +01:00
|
|
|
throw e
|
2020-09-11 01:14:22 +02:00
|
|
|
}
|
|
|
|
}
|
2022-03-24 00:32:58 +01:00
|
|
|
}
|
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2022-03-24 00:32:58 +01:00
|
|
|
private suspend fun buildIpcHandshake(ipcSubscriptionId: Int, ipcPublicationId: Int, connectionTimeoutSec: Int, reliable: Boolean): MediaDriverConnection {
|
2022-04-04 23:04:27 +02:00
|
|
|
if (remoteAddress == null) {
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.info { "IPC enabled." }
|
2022-04-04 23:04:27 +02:00
|
|
|
} else {
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.info { "IPC for loopback enabled and aeron is already running. Auto-changing network connection from ${IP.toString(remoteAddress!!)} -> IPC" }
|
2022-03-24 00:32:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// MAYBE the server doesn't have IPC enabled? If no, we need to connect via network instead
|
|
|
|
val ipcConnection = IpcMediaDriverConnection(streamIdSubscription = ipcSubscriptionId,
|
|
|
|
streamId = ipcPublicationId,
|
|
|
|
sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID
|
|
|
|
)
|
2021-04-27 10:28:36 +02:00
|
|
|
|
2022-03-24 00:32:58 +01:00
|
|
|
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports
|
|
|
|
try {
|
|
|
|
ipcConnection.buildClient(aeronDriver, logger)
|
|
|
|
return 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
|
2022-04-04 23:04:27 +02:00
|
|
|
val clientException = ClientException("Unable to connect via IPC to server. No address was specified", e)
|
|
|
|
ListenerManager.cleanStackTraceInternal(clientException)
|
|
|
|
throw clientException
|
2020-09-11 01:14:22 +02:00
|
|
|
}
|
2020-09-02 02:39:05 +02:00
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2022-04-04 23:04:27 +02:00
|
|
|
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 was specified")
|
|
|
|
ListenerManager.cleanStackTraceInternal(clientException)
|
|
|
|
throw clientException
|
|
|
|
}
|
|
|
|
|
2022-03-24 00:32:58 +01:00
|
|
|
logger.info { "IPC for loopback enabled, but unable to connect. Retrying with address ${IP.toString(remoteAddress!!)}" }
|
|
|
|
|
|
|
|
// try a UDP connection instead
|
|
|
|
val udpConnection = UdpMediaDriverClientConnection(
|
|
|
|
address = remoteAddress!!,
|
|
|
|
publicationPort = config.subscriptionPort,
|
|
|
|
subscriptionPort = config.publicationPort,
|
|
|
|
streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID,
|
|
|
|
sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID,
|
|
|
|
connectionTimeoutSec = connectionTimeoutSec,
|
|
|
|
isReliable = reliable
|
|
|
|
)
|
|
|
|
|
|
|
|
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports
|
|
|
|
udpConnection.buildClient(aeronDriver, logger)
|
|
|
|
return udpConnection
|
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2022-03-24 00:32:58 +01:00
|
|
|
private suspend fun buildUdpHandshake(connectionTimeoutSec: Int, reliable: Boolean): MediaDriverConnection {
|
|
|
|
val test = UdpMediaDriverClientConnection(
|
|
|
|
address = remoteAddress!!,
|
|
|
|
publicationPort = config.subscriptionPort,
|
|
|
|
subscriptionPort = config.publicationPort,
|
|
|
|
streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID,
|
|
|
|
sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID,
|
|
|
|
connectionTimeoutSec = connectionTimeoutSec,
|
|
|
|
isReliable = reliable
|
|
|
|
)
|
|
|
|
|
|
|
|
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports
|
|
|
|
test.buildClient(aeronDriver, logger)
|
|
|
|
return test
|
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2022-03-24 00:32:58 +01:00
|
|
|
// the handshake process might have to restart this connection process.
|
|
|
|
private suspend fun connect0(handshake: ClientHandshake<CONNECTION>, handshakeConnection: MediaDriverConnection, connectionTimeoutSec: Int) {
|
2020-09-02 02:39:05 +02:00
|
|
|
// this will block until the connection timeout, and throw an exception if we were unable to connect with the server
|
2022-03-24 00:32:58 +01:00
|
|
|
val isUsingIPC = handshakeConnection is IpcMediaDriverConnection
|
|
|
|
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2021-07-06 15:38:53 +02:00
|
|
|
// throws(ConnectTimedOutException::class, ClientRejectedException::class, ClientException::class)
|
2022-04-04 16:30:05 +02:00
|
|
|
val connectionInfo = handshake.hello(handshakeConnection, connectionTimeoutSec)
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
// VALIDATE:: check to see if the remote connection's public key has changed!
|
2020-09-22 19:41:19 +02:00
|
|
|
val validateRemoteAddress = if (isUsingIPC) {
|
2020-09-02 02:39:05 +02:00
|
|
|
PublicKeyValidationState.VALID
|
|
|
|
} else {
|
2022-05-30 02:45:50 +02:00
|
|
|
crypto.validateRemoteAddress(remoteAddress!!, remoteAddressString, connectionInfo.publicKey)
|
2020-09-02 02:39:05 +02:00
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
if (validateRemoteAddress == PublicKeyValidationState.INVALID) {
|
|
|
|
handshakeConnection.close()
|
2020-09-11 01:14:22 +02:00
|
|
|
val exception = ClientRejectedException("Connection to ${IP.toString(remoteAddress!!)} not allowed! Public key mismatch.")
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error(exception) { "Validation error" }
|
2020-09-02 02:39:05 +02:00
|
|
|
throw exception
|
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-08-25 17:45:08 +02:00
|
|
|
|
2022-03-24 00:32:58 +01:00
|
|
|
// VALIDATE:: If the serialization DOES NOT match between the client/server, then the server will emit a log, and the
|
2022-06-04 01:32:58 +02:00
|
|
|
// client will timeout. SPECIFICALLY.... we do not give class serialization/registration info to the client - in case the client
|
2020-09-02 02:39:05 +02:00
|
|
|
// is rogue, we do not want to carelessly provide info.
|
|
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
IpcMediaDriverConnection(
|
|
|
|
sessionId = connectionInfo.sessionId,
|
|
|
|
// NOTE: pub/sub must be switched!
|
|
|
|
streamIdSubscription = connectionInfo.publicationPort,
|
|
|
|
streamId = connectionInfo.subscriptionPort)
|
2020-08-12 23:30:16 +02:00
|
|
|
}
|
|
|
|
else {
|
2021-04-27 10:28:36 +02:00
|
|
|
UdpMediaDriverClientConnection(
|
|
|
|
address = (handshakeConnection as UdpMediaDriverClientConnection).address,
|
|
|
|
// NOTE: pub/sub must be switched!
|
|
|
|
publicationPort = connectionInfo.subscriptionPort,
|
|
|
|
subscriptionPort = connectionInfo.publicationPort,
|
|
|
|
streamId = connectionInfo.streamId,
|
|
|
|
sessionId = connectionInfo.sessionId,
|
2022-03-08 08:41:06 +01:00
|
|
|
connectionTimeoutSec = connectionTimeoutSec,
|
2021-04-27 10:28:36 +02:00
|
|
|
isReliable = handshakeConnection.isReliable)
|
2020-09-02 02:39:05 +02:00
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
// we have to construct how the connection will communicate!
|
2021-04-27 13:57:47 +02:00
|
|
|
clientConnection.buildClient(aeronDriver, logger)
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
// only the client connects to the server, so here we have to connect. The server (when creating the new "connection" object)
|
|
|
|
// does not need to do anything
|
|
|
|
//
|
|
|
|
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server-assigned client ports
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.info { clientConnection.clientInfo }
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-02 15:03:57 +02:00
|
|
|
|
|
|
|
///////////////
|
|
|
|
//// RMI
|
|
|
|
///////////////
|
|
|
|
|
2022-03-24 00:32:58 +01:00
|
|
|
// we set up our kryo information once we connect to a server (using the server's kryo registration details)
|
2020-09-15 21:17:47 +02:00
|
|
|
if (!serialization.finishInit(type, connectionInfo.kryoRegistrationDetails)) {
|
2020-09-02 15:03:57 +02:00
|
|
|
handshakeConnection.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)
|
2020-09-22 19:41:19 +02:00
|
|
|
val exception = if (isUsingIPC) {
|
2020-09-19 22:06:54 +02:00
|
|
|
ClientRejectedException("Connection to IPC has incorrect class registration details!!")
|
|
|
|
} else {
|
|
|
|
ClientRejectedException("Connection to ${IP.toString(remoteAddress!!)} has incorrect class registration details!!")
|
|
|
|
}
|
2022-04-04 23:04:27 +02:00
|
|
|
ListenerManager.cleanStackTraceInternal(exception)
|
2020-09-02 15:03:57 +02:00
|
|
|
throw exception
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-07-06 15:38:53 +02:00
|
|
|
|
2020-09-19 22:06:54 +02:00
|
|
|
val newConnection: CONNECTION
|
2020-09-22 19:41:19 +02:00
|
|
|
if (isUsingIPC) {
|
2021-07-09 15:16:12 +02:00
|
|
|
newConnection = connectionFunc(ConnectionParams(this, clientConnection, PublicKeyValidationState.VALID, rmiConnectionSupport))
|
2020-09-02 02:39:05 +02:00
|
|
|
} else {
|
2021-07-09 15:16:12 +02:00
|
|
|
newConnection = connectionFunc(ConnectionParams(this, clientConnection, validateRemoteAddress, rmiConnectionSupport))
|
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.close()
|
2022-03-24 00:32:58 +01:00
|
|
|
val exception = ClientRejectedException("Connection to ${IP.toString(remoteAddress!!)} was not permitted!")
|
2021-04-27 13:57:47 +02:00
|
|
|
ListenerManager.cleanStackTrace(exception)
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error(exception) { "Permission error" }
|
2020-09-19 22:06:54 +02:00
|
|
|
throw exception
|
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.info { "Adding new signature for ${IP.toString(remoteAddress!!)} : ${connectionInfo.publicKey.toHexString()}" }
|
2022-03-24 00:32:58 +01:00
|
|
|
storage.addRegisteredServerKey(remoteAddress!!, connectionInfo.publicKey)
|
2020-09-02 02:39:05 +02:00
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-19 22:06:54 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
//////////////
|
|
|
|
/// Extra Close action
|
|
|
|
//////////////
|
2022-06-06 17:18:46 +02:00
|
|
|
newConnection.closeAction = {
|
2020-09-02 02:39:05 +02:00
|
|
|
// this is called whenever connection.close() is called by the framework or via client.close()
|
2021-04-29 02:10:34 +02:00
|
|
|
|
2022-03-24 00:32:58 +01:00
|
|
|
// on the client, we want to GUARANTEE that the disconnect happens-before connect.
|
2021-04-30 16:01:25 +02:00
|
|
|
if (!lockStepForConnect.compareAndSet(null, SuspendWaiter())) {
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error { "Connection ${newConnection.id} : close lockStep for disconnect was in the wrong state!" }
|
2021-04-29 02:10:34 +02:00
|
|
|
}
|
2022-06-06 17:18:46 +02:00
|
|
|
|
2021-04-30 16:01:25 +02:00
|
|
|
isConnected = false
|
2020-09-02 02:39:05 +02:00
|
|
|
// this is called whenever connection.close() is called by the framework or via client.close()
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
// make sure to call our client.notifyDisconnect() callbacks
|
2020-08-25 17:45:08 +02:00
|
|
|
|
2021-04-30 18:15:54 +02:00
|
|
|
// this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback
|
|
|
|
actionDispatch.eventLoop {
|
2020-09-03 01:31:08 +02:00
|
|
|
listenerManager.notifyDisconnect(connection)
|
2022-03-24 00:32:58 +01:00
|
|
|
lockStepForConnect.getAndSet(null)?.cancel()
|
2020-09-02 02:39:05 +02:00
|
|
|
}
|
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-03 01:31:08 +02:00
|
|
|
connection0 = newConnection
|
2020-09-11 01:14:22 +02:00
|
|
|
addConnection(newConnection)
|
2020-08-25 02:43:20 +02:00
|
|
|
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error { "Connection created, finishing handshake: ${handshake.connectKey}" }
|
2022-03-15 20:43:43 +01:00
|
|
|
|
2020-09-03 01:31:08 +02:00
|
|
|
// tell the server our connection handshake is done, and the connection can now listen for data.
|
2020-09-11 11:06:53 +02:00
|
|
|
// also closes the handshake (will also throw connect timeout exception)
|
2022-03-15 20:43:43 +01:00
|
|
|
val canFinishConnecting: Boolean
|
|
|
|
runBlocking {
|
2022-03-24 00:32:58 +01:00
|
|
|
// this value matches the server, and allows for a more robust connection attempt
|
|
|
|
val successAttemptTimeout = config.connectionCloseTimeoutInSeconds * 2
|
2022-03-15 20:43:43 +01:00
|
|
|
canFinishConnecting = try {
|
2022-03-24 00:32:58 +01:00
|
|
|
handshake.done(handshakeConnection, successAttemptTimeout)
|
2022-03-15 20:43:43 +01:00
|
|
|
} catch (e: ClientException) {
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error(e) { "Error during handshake" }
|
2022-03-15 20:43:43 +01:00
|
|
|
false
|
|
|
|
}
|
2021-07-06 15:38:53 +02:00
|
|
|
}
|
|
|
|
|
2020-09-03 01:31:08 +02:00
|
|
|
if (canFinishConnecting) {
|
|
|
|
isConnected = true
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2021-04-30 16:01:25 +02:00
|
|
|
// this forces the current thread to WAIT until poll system has started
|
|
|
|
val waiter = SuspendWaiter()
|
|
|
|
|
2020-09-03 01:31:08 +02:00
|
|
|
// 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
|
2021-04-29 02:10:34 +02:00
|
|
|
|
2020-09-03 14:36:24 +02:00
|
|
|
// these have to be in two SEPARATE actionDispatch.launch commands.... otherwise...
|
2022-04-04 16:30:05 +02:00
|
|
|
// if something inside-of notifyConnect is blocking or suspends, then polling will never happen!
|
2020-09-03 14:36:24 +02:00
|
|
|
actionDispatch.launch {
|
2021-04-30 16:01:25 +02:00
|
|
|
waiter.doNotify()
|
|
|
|
|
2020-09-03 01:31:08 +02:00
|
|
|
val pollIdleStrategy = config.pollIdleStrategy
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-03 01:31:08 +02:00
|
|
|
while (!isShutdown()) {
|
2020-09-23 17:08:25 +02:00
|
|
|
if (newConnection.isClosedViaAeron()) {
|
2020-09-11 11:06:53 +02:00
|
|
|
// If the connection has either been closed, or has expired, it needs to be cleaned-up/deleted.
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.debug { "[${newConnection.id}] connection expired" }
|
2020-09-15 12:01:05 +02:00
|
|
|
|
2021-07-06 15:38:53 +02:00
|
|
|
// event-loop is required, because we want to run this code AFTER the current coroutine has finished. This prevents
|
|
|
|
// odd race conditions when a client is restarted. Can only be run from inside another co-routine!
|
2021-04-30 16:01:25 +02:00
|
|
|
actionDispatch.eventLoop {
|
|
|
|
// NOTE: We do not shutdown the client!! The client is only closed by explicitly calling `client.close()`
|
|
|
|
newConnection.close()
|
|
|
|
}
|
2020-09-03 01:31:08 +02:00
|
|
|
return@launch
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Polls the AERON media driver subscription channel for incoming messages
|
|
|
|
val pollCount = newConnection.pollSubscriptions()
|
2020-09-02 02:39:05 +02:00
|
|
|
|
2020-09-03 01:31:08 +02:00
|
|
|
// 0 means we idle. >0 means reset and don't idle (because there are likely more poll events)
|
|
|
|
pollIdleStrategy.idle(pollCount)
|
|
|
|
}
|
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
}
|
2021-04-30 16:01:25 +02:00
|
|
|
|
|
|
|
actionDispatch.eventLoop {
|
|
|
|
waiter.doWait()
|
|
|
|
|
|
|
|
lockStepForConnect.value?.doWait()
|
|
|
|
|
|
|
|
listenerManager.notifyConnect(newConnection)
|
|
|
|
|
|
|
|
lockStepForConnect.lazySet(null)
|
|
|
|
}
|
2020-09-02 02:39:05 +02:00
|
|
|
} else {
|
|
|
|
close()
|
2021-07-06 15:38:53 +02:00
|
|
|
|
2022-05-30 02:45:50 +02:00
|
|
|
val exception = ClientRejectedException("Unable to connect with server: ${handshakeConnection.clientInfo}")
|
2020-09-02 02:39:05 +02:00
|
|
|
ListenerManager.cleanStackTrace(exception)
|
|
|
|
throw exception
|
2020-08-12 23:30:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
/**
|
2020-09-03 01:31:08 +02:00
|
|
|
* true if the remote public key changed. This can be useful if specific actions are necessary when the key has changed.
|
2020-09-02 02:39:05 +02:00
|
|
|
*/
|
2020-09-03 01:31:08 +02:00
|
|
|
val remoteKeyHasChanged: Boolean
|
|
|
|
get() = connection.hasRemoteKeyChanged()
|
2020-08-12 23:30:43 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
/**
|
2020-09-09 01:33:09 +02:00
|
|
|
* the remote address
|
2020-09-02 02:39:05 +02:00
|
|
|
*/
|
2020-09-09 01:33:09 +02:00
|
|
|
val remoteAddress: InetAddress?
|
2020-09-03 01:31:08 +02:00
|
|
|
get() = remoteAddress0
|
2020-08-12 23:30:43 +02:00
|
|
|
|
2020-09-09 01:33:09 +02:00
|
|
|
/**
|
|
|
|
* the remote address, as a string.
|
|
|
|
*/
|
|
|
|
val remoteAddressString: String
|
2022-05-30 02:45:50 +02:00
|
|
|
get() {
|
|
|
|
return when (val address = remoteAddress) {
|
|
|
|
is Inet4Address -> IPv4.toString(address)
|
|
|
|
is Inet6Address -> IPv6.toString(address, true)
|
|
|
|
else -> "ipc"
|
|
|
|
}
|
|
|
|
}
|
2020-09-09 01:33:09 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
/**
|
2020-09-03 01:31:08 +02:00
|
|
|
* true if this connection is an IPC connection
|
2020-09-02 02:39:05 +02:00
|
|
|
*/
|
2020-09-03 01:31:08 +02:00
|
|
|
val isIPC: Boolean
|
|
|
|
get() = connection.isIpc
|
2020-08-12 23:30:43 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
/**
|
|
|
|
* @return true if this connection is a network connection
|
|
|
|
*/
|
2020-09-03 01:31:08 +02:00
|
|
|
val isNetwork: Boolean
|
|
|
|
get() = connection.isNetwork
|
2020-08-12 23:30:43 +02:00
|
|
|
|
2020-09-02 02:39:05 +02:00
|
|
|
/**
|
|
|
|
* @return the connection (TCP or IPC) id of this connection.
|
|
|
|
*/
|
2020-09-03 01:31:08 +02:00
|
|
|
val id: Int
|
|
|
|
get() = connection.id
|
2020-08-12 23:30:16 +02:00
|
|
|
|
|
|
|
/**
|
2020-09-03 01:31:08 +02:00
|
|
|
* the connection used by the client, this is only valid after the client has connected
|
2020-08-12 23:30:16 +02:00
|
|
|
*/
|
2020-09-03 01:31:08 +02:00
|
|
|
val connection: CONNECTION
|
|
|
|
get() = connection0 as CONNECTION
|
|
|
|
|
2020-08-12 23:30:16 +02:00
|
|
|
|
|
|
|
/**
|
2020-09-22 19:42:04 +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
|
|
|
|
*/
|
|
|
|
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!")
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error(exception) { "No connection!" }
|
2020-09-22 19:42:04 +02:00
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-04-29 10:25:25 +02:00
|
|
|
* Sends a message to the server, if the connection is closed for any reason, this returns false.
|
2020-09-22 19:42:04 +02:00
|
|
|
*
|
2021-04-29 10:25:25 +02:00
|
|
|
* @return true if the message was sent successfully, false if the connection has been closed
|
|
|
|
*/
|
|
|
|
fun sendBlocking(message: Any): Boolean {
|
|
|
|
return runBlocking {
|
|
|
|
send(message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection.
|
2020-09-22 19:42:04 +02:00
|
|
|
*
|
2021-04-29 10:25:25 +02:00
|
|
|
* @param function called when the ping returns (ie: update time/latency counters/metrics/etc)
|
2021-07-06 15:38:53 +02:00
|
|
|
*
|
|
|
|
* @return true if the ping was successfully sent to the client
|
2020-08-12 23:30:16 +02:00
|
|
|
*/
|
2021-07-08 14:33:24 +02:00
|
|
|
suspend fun ping(pingTimeoutSeconds: Int = PingManager.DEFAULT_TIMEOUT_SECONDS, function: suspend Ping.() -> Unit): Boolean {
|
2020-09-03 01:31:08 +02:00
|
|
|
val c = connection0
|
2021-04-29 10:25:25 +02:00
|
|
|
|
2020-08-12 23:30:16 +02:00
|
|
|
if (c != null) {
|
2021-07-08 14:33:24 +02:00
|
|
|
return pingManager.ping(c, pingTimeoutSeconds, actionDispatch, responseManager, logger, function)
|
2020-08-12 23:30:16 +02:00
|
|
|
} else {
|
2022-05-30 02:45:50 +02:00
|
|
|
logger.error(ClientException("Cannot send a ping when there is no connection!")) { "No connection!" }
|
2020-08-12 23:30:16 +02:00
|
|
|
}
|
2021-07-06 15:38:53 +02:00
|
|
|
|
|
|
|
return false
|
2020-08-12 23:30:16 +02:00
|
|
|
}
|
|
|
|
|
2020-08-12 23:30:43 +02:00
|
|
|
/**
|
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)
|
2020-08-12 23:30:43 +02:00
|
|
|
*/
|
2021-07-08 14:33:24 +02:00
|
|
|
fun pingBlocking(pingTimeoutSeconds: Int = PingManager.DEFAULT_TIMEOUT_SECONDS, function: suspend Ping.() -> Unit): Boolean {
|
2021-07-06 15:38:53 +02:00
|
|
|
return runBlocking {
|
2021-07-08 14:33:24 +02:00
|
|
|
ping(pingTimeoutSeconds, function)
|
2021-04-29 10:25:25 +02:00
|
|
|
}
|
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
|
2020-09-03 01:31:08 +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) {
|
2021-04-29 10:02:59 +02:00
|
|
|
val savedPublicKey = storage.getRegisteredServerKey(address)
|
2020-08-12 23:30:16 +02:00
|
|
|
if (savedPublicKey != null) {
|
2020-09-09 01:33:09 +02:00
|
|
|
logger.debug { "Deleting remote IP address key $address" }
|
2021-04-29 10:02:59 +02:00
|
|
|
storage.removeRegisteredServerKey(address)
|
2020-08-12 23:30:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-25 14:49:17 +02:00
|
|
|
// no impl
|
2021-07-06 15:38:53 +02:00
|
|
|
final override fun close0() {
|
|
|
|
// when we close(), don't permit reconnect. add "close(boolean)" (aka "shutdown"), to deny a connect request (and permanently stay closed)
|
2020-08-12 23:30:43 +02:00
|
|
|
}
|
2020-08-12 23:30:16 +02:00
|
|
|
}
|