Network/src/dorkbox/network/Server.kt

435 lines
19 KiB
Kotlin

/*
* 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.netUtil.IPv4
import dorkbox.netUtil.IPv6
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.AeronPoller
import dorkbox.network.connection.Connection
import dorkbox.network.connection.ConnectionParams
import dorkbox.network.connection.EndPoint
import dorkbox.network.connectionType.ConnectionRule
import dorkbox.network.exceptions.AllocationException
import dorkbox.network.exceptions.ServerException
import dorkbox.network.handshake.ServerHandshake
import dorkbox.network.handshake.ServerHandshakePollers
import dorkbox.network.rmi.RmiSupportServer
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.net.InetAddress
import java.util.concurrent.*
/**
* The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the
* server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections())
*
* To put it bluntly, ONLY have the server do work inside a listener!
*
* @param config these are the specific connection options
* @param connectionFunc allows for custom connection implementations defined as a unit function
* @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints)
*/
open class Server<CONNECTION : Connection>(
config: ServerConfiguration = ServerConfiguration(),
connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION,
loggerName: String = Server::class.java.simpleName)
: EndPoint<CONNECTION>(config, connectionFunc, loggerName) {
/**
* The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the
* server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections())
*
* To put it bluntly, ONLY have the server do work inside a listener!
*
* @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: ServerConfiguration,
loggerName: String,
connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION)
: this(config, connectionFunc, loggerName)
/**
* The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the
* server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections())
*
* To put it bluntly, ONLY have the server do work inside of a listener!
*
* @param config these are the specific connection options
* @param connectionFunc allows for custom connection implementations defined as a unit function
*/
constructor(config: ServerConfiguration,
connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION)
: this(config, connectionFunc, Server::class.java.simpleName)
/**
* The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the
* server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections())
*
* To put it bluntly, ONLY have the server do work inside of a listener!
*
* @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: ServerConfiguration,
loggerName: String = Server::class.java.simpleName)
: this(config,
{
@Suppress("UNCHECKED_CAST")
Connection(it) as CONNECTION
},
loggerName)
/**
* The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the
* server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections())
*
* To put it bluntly, ONLY have the server do work inside of a listener!
*
* @param config these are the specific connection options
*/
constructor(config: ServerConfiguration)
: this(config,
{
@Suppress("UNCHECKED_CAST")
Connection(it) as CONNECTION
},
Server::class.java.simpleName)
companion object {
/**
* Gets the version number.
*/
const val version = "6.4"
/**
* Checks to see if a server (using the specified configuration) is running.
*
* This method should only be used to check if a server is running for a DIFFERENT configuration than the currently running server
*/
fun isRunning(configuration: ServerConfiguration): Boolean {
return AeronDriver(configuration).isRunning()
}
init {
// Add this project to the updates system, which verifies this class + UUID + version information
dorkbox.updates.Updates.add(Server::class.java, "90a2c3b1e4fa41ea90d31fbdf8b2c6ef", version)
}
}
/**
* Methods supporting Remote Method Invocation and Objects for GLOBAL scope objects (different than CONNECTION scope objects)
*/
val rmiGlobal = RmiSupportServer(logger, rmiGlobalSupport)
/**
* @return true if this server has successfully bound to an IP address and is running
*/
private var bindAlreadyCalled = atomic(false)
/**
* These are run in lock-step to shutdown/close the server. Afterwards, bind() can be called again
*/
@Volatile
private var shutdownPollLatch = CountDownLatch(1)
@Volatile
private var shutdownEventLatch = CountDownLatch(1)
/**
* Maintains a thread-safe collection of rules used to define the connection type with this server.
*/
private val connectionRules = CopyOnWriteArrayList<ConnectionRule>()
/**
* true if the following network stacks are available for use
*/
internal val canUseIPv4 = config.enableIPv4 && IPv4.isAvailable
internal val canUseIPv6 = config.enableIPv6 && IPv6.isAvailable
// localhost/loopback IP might not always be 127.0.0.1 or ::1
// We want to listen on BOTH IPv4 and IPv6 (config option lets us configure this)
internal val listenIPv4Address: InetAddress? =
if (canUseIPv4) {
formatCommonAddress(config.listenIpAddress, true) { null } // if it's not a valid IP, the lambda will return null
}
else {
null
}
internal val listenIPv6Address: InetAddress? =
if (canUseIPv6) {
formatCommonAddress(config.listenIpAddress, false) { null } // if it's not a valid IP, the lambda will return null
}
else {
null
}
final override fun newException(message: String, cause: Throwable?): Throwable {
return ServerException(message, cause)
}
/**
* Binds the server to AERON configuration
*/
@Suppress("DuplicatedCode")
fun bind() {
// NOTE: it is critical to remember that Aeron DOES NOT like running from coroutines!
if (bindAlreadyCalled.getAndSet(true)) {
logger.error { "Unable to bind when the server is already running!" }
return
}
try {
startDriver()
} catch (e: Exception) {
logger.error(e) { "Unable to start the network driver" }
return
}
shutdownPollLatch = CountDownLatch(1)
shutdownEventLatch = CountDownLatch(1)
config as ServerConfiguration
// we are done with initial configuration, now initialize aeron and the general state of this endpoint
// this forces the current thread to WAIT until poll system has started
val pollStartupLatch = CountDownLatch(1)
val server = this@Server
val handshake = ServerHandshake(logger, config, listenerManager, aeronDriver)
val ipcPoller: AeronPoller = ServerHandshakePollers.ipc(aeronDriver, config, server, handshake)
// if we are binding to WILDCARD, then we have to do something special if BOTH IPv4 and IPv6 are enabled!
val isWildcard = listenIPv4Address == IPv4.WILDCARD || listenIPv6Address == IPv6.WILDCARD
val ipv4Poller: AeronPoller
val ipv6Poller: AeronPoller
if (isWildcard) {
if (canUseIPv4 && canUseIPv6) {
// IPv6 will bind to IPv4 wildcard as well, so don't bind both!
ipv4Poller = ServerHandshakePollers.disabled("IPv4 Disabled")
ipv6Poller = ServerHandshakePollers.ip6Wildcard(aeronDriver, config, server, handshake)
} else {
// only 1 will be a real poller
ipv4Poller = ServerHandshakePollers.ip4(aeronDriver, config, server, handshake)
ipv6Poller = ServerHandshakePollers.ip6(aeronDriver, config, server, handshake)
}
} else {
ipv4Poller = ServerHandshakePollers.ip4(aeronDriver, config, server, handshake)
ipv6Poller = ServerHandshakePollers.ip6(aeronDriver, config, server, handshake)
}
val networkEventProcessor = Runnable {
pollStartupLatch.countDown()
val pollIdleStrategy = config.pollIdleStrategy.cloneToNormal()
try {
var pollCount: Int
while (!isShutdown()) {
pollCount = 0
// NOTE: regarding fragment limit size. Repeated calls to '.poll' will reassemble a fragment.
// `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)`
// this checks to see if there are NEW clients on the handshake ports
pollCount += ipv4Poller.poll()
pollCount += ipv6Poller.poll()
// this checks to see if there are NEW clients via IPC
pollCount += ipcPoller.poll()
// this manages existing clients (for cleanup + connection polling). This has a concurrent iterator,
// so we can modify this as we go
connections.forEach { connection ->
if (!connection.isClosedViaAeron()) {
// Otherwise, poll the connection for messages
pollCount += connection.poll()
} else {
// If the connection has either been closed, or has expired, it needs to be cleaned-up/deleted.
logger.debug { "[${connection.id}/${connection.streamId}] connection expired" }
removeConnection(connection)
// this will call removeConnection again, but that is ok
// this is blocking, because the connection MUST be removed in the same thread that is processing events
connection.close()
// have to manually notify the server-listenerManager that this connection was closed
// if the connection was MANUALLY closed (via calling connection.close()), then the connection-listenermanager is
// instantly notified and on cleanup, the server-listenermanager is called
// this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback
actionDispatch.launch {
listenerManager.notifyDisconnect(connection)
}
}
}
// 0 means we idle. >0 means reset and don't idle (because there are likely more poll events)
pollIdleStrategy.idle(pollCount)
}
logger.debug { "Network event dispatch closing..." }
// we want to process **actual** close cleanup events on this thread as well, otherwise we will have threading problems
shutdownPollLatch.await()
// we have to manually cleanup the connections and call server-notifyDisconnect because otherwise this will never get called
val jobs = mutableListOf<Job>()
// we want to clear all the connections FIRST (since we are shutting down)
val cons = mutableListOf<CONNECTION>()
connections.forEach { cons.add(it) }
connections.clear()
cons.forEach { connection ->
logger.info { "[${connection.id}/${connection.streamId}] Connection cleanup and close" }
// make sure the connection is closed (close can only happen once, so a duplicate call does nothing!)
connection.close()
// have to manually notify the server-listenerManager that this connection was closed
// if the connection was MANUALLY closed (via calling connection.close()), then the connection-listenermanager is
// instantly notified and on cleanup, the server-listenermanager is called
// NOTE: this must be the LAST thing happening!
// this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback
val job = actionDispatch.launch {
listenerManager.notifyDisconnect(connection)
}
jobs.add(job)
}
// when we close a client or a server, we want to make sure that ALL notifications are finished.
// when it's just a connection getting closed, we don't care about this. We only care when it's "global" shutdown
runBlocking {
jobs.forEach { it.join() }
}
} catch (e: Exception) {
logger.error(e) { "Unexpected error during server message polling!" }
} finally {
ipv4Poller.close()
ipv6Poller.close()
ipcPoller.close()
// clear all the handshake info
handshake.clear()
try {
// make sure that we have de-allocated all connection data
handshake.checkForMemoryLeaks()
} catch (e: AllocationException) {
logger.error(e) { "Error during server cleanup" }
}
// finish closing -- this lets us make sure that we don't run into race conditions on the thread that calls close()
try {
shutdownEventLatch.countDown()
} catch (ignored: Exception) {}
}
}
config.networkInterfaceEventDispatcher.submit(networkEventProcessor)
// wait for the polling thread to startup before letting bind() return
pollStartupLatch.await()
}
/**
* Adds an IP+subnet rule that defines what type of connection this IP+subnet should have.
* - NOTHING : Nothing happens to the in/out bytes
* - COMPRESS: The in/out bytes are compressed with LZ4-fast
* - COMPRESS_AND_ENCRYPT: The in/out bytes are compressed (LZ4-fast) THEN encrypted (AES-256-GCM)
*
* If no rules are defined, then for LOOPBACK, it will always be `COMPRESS` and for everything else it will always be `COMPRESS_AND_ENCRYPT`.
*
* If rules are defined, then everything by default is `COMPRESS_AND_ENCRYPT`.
*
* The compression algorithm is LZ4-fast, so there is a small performance impact for a very large gain
* Compress : 6.210 micros/op; 629.0 MB/s (output: 55.4%)
* Uncompress : 0.641 micros/op; 6097.9 MB/s
*/
fun addConnectionRules(vararg rules: ConnectionRule) {
connectionRules.addAll(listOf(*rules))
}
/**
* Runs an action for each connection
*/
fun forEachConnection(function: (connection: CONNECTION) -> Unit) {
connections.forEach {
function(it)
}
}
/**
* Closes the server and all it's connections. After a close, you may call 'bind' again.
*/
final override fun close0() {
// when we call close, it will shut-down the polling mechanism, then wait for us to tell it to clean-up connections.
//
// Aeron + the Media Driver will have already been shutdown at this point.
if (bindAlreadyCalled.getAndSet(false)) {
// These are run in lock-step
shutdownPollLatch.countDown()
shutdownEventLatch.await()
}
}
// /**
// * Only called by the server!
// *
// * If we are loopback or the client is a specific IP/CIDR address, then we do things differently. The LOOPBACK address will never encrypt or compress the traffic.
// */
// // after the handshake, what sort of connection do we want (NONE, COMPRESS, ENCRYPT+COMPRESS)
// fun getConnectionUpgradeType(remoteAddress: InetSocketAddress): Byte {
// val address = remoteAddress.address
// val size = connectionRules.size
//
// // if it's unknown, then by default we encrypt the traffic
// var connectionType = ConnectionProperties.COMPRESS_AND_ENCRYPT
// if (size == 0 && address == IPv4.LOCALHOST) {
// // if nothing is specified, then by default localhost is compression and everything else is encrypted
// connectionType = ConnectionProperties.COMPRESS
// }
// for (i in 0 until size) {
// val rule = connectionRules[i] ?: continue
// if (rule.matches(remoteAddress)) {
// connectionType = rule.ruleType()
// break
// }
// }
// logger.debug("Validating {} Permitted type is: {}", remoteAddress, connectionType)
// return connectionType.type
// }
}