Compare commits

...

637 Commits

Author SHA1 Message Date
Robinson 3f77af5894
More responsive shutdown logic during handshake and fixed crashes when forcing a shutdown 2024-02-19 10:09:03 +01:00
Robinson c197a2f627
logging 2024-02-05 21:58:00 +01:00
Robinson 0500de29c8
updated copyright 2024-02-05 21:57:40 +01:00
Robinson 4fc0cb7541
fixed comments 2024-01-15 10:39:58 +01:00
Robinson e4721b4c7c
fixed typo 2023-12-14 22:50:42 +01:00
Robinson bd6476059b
ConnectionCounts are now threadsafe 2023-12-12 13:31:07 +01:00
Robinson e80cc93c72
Use ThreadSafe publication instead of exclusive publication 2023-12-12 13:25:01 +01:00
Robinson a706cdb228
If we have "max connections" specified, then obey the limit 2023-12-12 13:24:33 +01:00
Robinson 98d8321902
updated config comments 2023-12-12 13:24:01 +01:00
Robinson 331f921df0
Added extra information for handshake/connection timeout potential issues 2023-12-12 13:23:51 +01:00
Robinson 9c2fa4b65b
updated logging 2023-12-12 13:22:28 +01:00
Robinson db15b62c8c
Added more unit tests, increased multi-threading for mutli-client unit test 2023-12-04 13:37:18 +01:00
Robinson 6f50618040
Now properly waits for event dispatcher to shutdown in unit tests 2023-12-04 13:36:47 +01:00
Robinson 6bbf62f886
Added more unit tests for aeron connectivity 2023-12-04 11:10:52 +01:00
Robinson cdc056f3a1
logging tweak 2023-12-04 10:47:51 +01:00
Robinson cb7f8b2990
Send all buffered messages at once, instead of 1-at-a-time. 2023-12-04 10:47:41 +01:00
Robinson b9f7a552f0
update license 2023-11-28 21:39:14 +01:00
Robinson 2c9d3c119c
Updated build deps 2023-11-28 21:08:53 +01:00
Robinson 3a5295efe8
Cleaned logging 2023-11-28 20:53:37 +01:00
Robinson 5c4d64f3f1
fixed up comments 2023-11-28 20:52:58 +01:00
Robinson 41b3acf147
Fixed api name 2023-11-27 14:40:09 +01:00
Robinson cf2e7ffc77
Removed exception stacktrace during reconnect 2023-11-27 13:11:47 +01:00
Robinson bf0cd3f0e6
Added support for PER-CONNECTION buffering of messages (default is enabled) 2023-11-27 11:14:52 +01:00
Robinson f1a06fd8fd
Better comments/docs 2023-11-27 11:13:54 +01:00
Robinson 13e8501255
Commented out remaining logic for connection rules (not impl yet) 2023-11-27 11:12:53 +01:00
Robinson a1db866375
updated deps 2023-11-22 20:40:05 +01:00
Robinson b496f83e64
100 concurrent connections in a unit tests kills the machine. 2023-11-22 20:39:06 +01:00
Robinson 2cfc2e41e6
Make sure now that errors during unit tests are properly failing (or ignoring) as appropriate the test. 2023-11-22 20:38:46 +01:00
Robinson 76f42c900c
Guarantee that connect occurs AFTER the current close events are finished running before redispatching on the connect dispatcher 2023-11-22 09:18:17 +01:00
Robinson 88bac6ef84
Version 6.15 2023-11-16 12:08:30 +01:00
Robinson f0493beca1
updated minlog 2023-11-13 22:31:04 +01:00
Robinson d4fd773ea0
spaces 2023-11-13 22:30:43 +01:00
Robinson 644d28ea70
Any exceptions will cause a unit test failure now 2023-11-13 18:45:17 +01:00
Robinson 35020adac9
Updated to 100 concurrent connections (on 50 separate threads) 2023-11-13 18:44:59 +01:00
Robinson bae5b41d1c
disconnect period is as short as possible to improve unit test performance 2023-11-13 14:15:13 +01:00
Robinson 8e32e0980c
Shutdown is now atomic instead of volatile 2023-11-13 14:10:19 +01:00
Robinson cbfe51f746
Added Handshake dispatch (was required, and must be single threaded) 2023-11-13 14:10:00 +01:00
Robinson 2aebbe6116
Added more logging 2023-11-08 12:44:15 +01:00
Robinson 4bd77515d8
Increased connection timeouts. 2023-11-07 16:48:05 +01:00
Robinson a5286899b7
When creating publications and handshaking, we CANNOT do this on the main processing thread 2023-11-03 18:21:14 +01:00
Robinson af19049519
Added multi-dispatch, for on the server when conducting handshakes (and waiting for a connection to complete). Under load, we cannot block the main thread 2023-11-03 18:15:34 +01:00
Robinson f40e8cf14d
More detailed error message 2023-11-03 18:14:48 +01:00
Robinson 2162131b17
updated shadowclass file 2023-11-02 22:37:09 +01:00
Robinson 91deea8b1a
Added support for callbacks on a message, so there can be 'happens-before' logic. 2023-11-02 22:36:50 +01:00
Robinson 58535a923b
Added support for connection tags (so the client can set a name for its connection, and the server will get that name). This is usefull for identifying different connections (and doing different things) based on their tag name. 2023-10-28 20:55:49 +02:00
Robinson fe98763712
All connections are now buffered - in the event there is a network issue, or a quick reconnect, and messages are sent DURING this disconnected phase, these messages will be resent on the new connection once it is connected 2023-10-28 20:54:40 +02:00
Robinson 27b4b0421e
disabled test debug 2023-10-26 21:19:50 +02:00
Robinson 0c4c442b3a
Fixed/cleaned up connection polling and restarts 2023-10-26 21:19:36 +02:00
Robinson ba57447169
fixed connect notify 2023-10-26 21:19:07 +02:00
Robinson 1b235e21aa
waiting for endpoint to shutdown better supports restarts 2023-10-26 21:14:05 +02:00
Robinson 046ece160f
code polish 2023-10-26 21:13:35 +02:00
Robinson 737b68549c
Wrapped potential RMI errors in exception catching 2023-10-26 21:13:21 +02:00
Robinson f531f61a53
Better support for polling and sending dc message 2023-10-26 21:12:58 +02:00
Robinson 7f2ad97aa7
code cleanup and comments 2023-10-26 14:57:48 +02:00
Robinson d7884c4d8d
Updated logs 2023-10-26 12:02:09 +02:00
Robinson 9d303beade
Better session management + logs 2023-10-26 09:30:11 +02:00
Robinson 70825708a3
More careful event dispatch (no longer global, but per endpoint) 2023-10-26 08:09:47 +02:00
Robinson 4b58a63dc1
Added extra (general) log message when a network error occurs 2023-10-24 20:38:20 +02:00
Robinson 495cb954d8
cleanAllStackTrace() returns itself 2023-10-24 15:14:39 +02:00
Robinson 59d17ea367
Better logic for unit test 2023-10-24 13:47:10 +02:00
Robinson 8c2b6b39cd
wait for close is not explicitly necessary 2023-10-24 13:46:46 +02:00
Robinson 60a26202b4
Added extra debug info 2023-10-24 13:46:28 +02:00
Robinson 044ce8771f
Fixed sigint close command issues 2023-10-24 13:46:16 +02:00
Robinson 90d087054e
Added for remote server testing 2023-10-24 13:45:58 +02:00
Robinson 4906e94aef
Code cleanup 2023-10-24 13:13:06 +02:00
Robinson 14544d3296
Removed delayed close from event poller 2023-10-24 12:06:55 +02:00
Robinson 2270b815b4
better logging 2023-10-23 23:27:55 +02:00
Robinson 706cf5b3e8
Fixed edge case with session connections and sending data on a publication that is not connected (either yet, or is an old one) 2023-10-23 23:24:57 +02:00
Robinson 01ab0bf1d8
Properly cleanup the remote object storage/cache 2023-10-23 23:23:32 +02:00
Robinson d40c080311
API parameter clarification 2023-10-19 23:42:36 +02:00
Robinson 83a9a5762d
Connection timeout is based from when connection is created 2023-10-19 23:42:08 +02:00
Robinson 0e16747dc2
logging 2023-10-19 23:41:43 +02:00
Robinson a15478c535
modified delayLingerTimeout() to be more intelligent with regards to the delay linger timeout 2023-10-18 20:00:54 +02:00
Robinson cfc08a2f4b
SessionManager expiration now using the correct expire time 2023-10-18 19:55:28 +02:00
Robinson c38fa13f11
More careful return values when adding data to aeron buffer 2023-10-18 19:54:33 +02:00
Robinson 7cacc63dca
renamed function: isConnected -> isClosedWithTimeout 2023-10-18 19:54:07 +02:00
Robinson d51c878a65
Fixed cast exception with sessions 2023-10-18 19:52:50 +02:00
Robinson 6fb5dbb833
added sendDisconnectMessage to API when closing 2023-10-18 19:52:31 +02:00
Robinson 2245f0bfc5
more comments 2023-10-18 19:50:04 +02:00
Robinson d4e3e2e41d
Better/easier checking if we are a session or not 2023-10-18 19:49:33 +02:00
Robinson 2a8ac38e55
ResponseManager now uses a special TimeoutException instead of generic exception. 2023-10-18 19:47:02 +02:00
Robinson 46cb174183
more logging 2023-10-18 19:46:34 +02:00
Robinson de6d22f808
Added logs when closing storage 2023-10-18 19:33:28 +02:00
Robinson 099f9de834
Enhanced logging for session connection type 2023-10-18 19:33:13 +02:00
Robinson 047d938386
Safely attempt to close the settings store (permissions might not allow it) 2023-10-17 22:54:34 +02:00
Robinson 4d09999f0a
Updated logging 2023-10-17 16:48:31 +02:00
Robinson ee296c602a
updated gradle 2023-10-17 16:48:02 +02:00
Robinson 3a69d1a525
updated build deps 2023-10-17 16:47:52 +02:00
Robinson 53f7cd8cf1
Simplified connection log info for debug output 2023-10-17 16:47:40 +02:00
Robinson c62016dad9
version 6.14 2023-10-05 13:19:10 +02:00
Robinson decec8641b
updated license 2023-10-05 13:18:40 +02:00
Robinson 66a922b6b5
Increased default macos devShm virtual drive 2023-10-05 13:18:28 +02:00
Robinson 7eac9699c9
Fixed session/connection lateinit errors 2023-10-05 13:18:00 +02:00
Robinson 8f9ee52b36
Fixed null pointer 2023-10-05 13:17:28 +02:00
Robinson 48325ee846
version 6.13 2023-10-03 22:11:48 +02:00
Robinson 1fded5575b
updated build deps 2023-10-03 22:11:40 +02:00
Robinson 1287eb8c6e
AeronDriverInternal will now restart the network if there is an `unexpected close of heartbeat timestamp counter` error 2023-10-03 22:01:54 +02:00
Robinson c69a33f1a9
version 6.12 2023-09-28 01:55:14 +02:00
Robinson 0825274bd0
Cleaned up ordering of connection initialization 2023-09-28 01:55:03 +02:00
Robinson b55168a3eb
Now safely try to close a connection when it's not possible (just log, don't throw exception) 2023-09-26 19:53:27 +02:00
Robinson 653236a7e2
version 6.11 2023-09-25 14:00:52 +02:00
Robinson b45826da80
Code cleanup + better unit tests 2023-09-25 13:59:46 +02:00
Robinson 78374e4dfc
More clearly defined session management. Fixed problem when reconnecting + RMI create callbacks. 2023-09-25 13:59:26 +02:00
Robinson b2217f66ee
Added additional cast method 2023-09-25 13:58:32 +02:00
Robinson 1b6880bf7d
Fixed issues when deleting RMI objects/proxies 2023-09-25 13:58:24 +02:00
Robinson babee06372
Fixed issues surrounding RMI + timeouts 2023-09-25 13:58:02 +02:00
Robinson 00d444bde7
Fixed out-of-order signalling 2023-09-25 13:57:04 +02:00
Robinson 8dd70e9e0e
Cleaned up comments 2023-09-25 13:56:23 +02:00
Robinson 72a7121762
More clear logging 2023-09-25 13:56:02 +02:00
Robinson e399f4948d
Stronger checks for RMI ID minimum values 2023-09-25 13:54:37 +02:00
Robinson 2ab8d7b3bd
Added comments 2023-09-25 13:54:11 +02:00
Robinson be78d498dc
Add connection before init, so init happens before polling of events 2023-09-25 13:53:37 +02:00
Robinson 8bbaa6df18
Cleaned up sessions 2023-09-22 15:54:01 +02:00
Robinson c0227fee06
Updated deps, now pure support for jpms 2023-09-21 12:55:09 +02:00
Robinson 1125785b21
added unit test for sessions 2023-09-21 12:54:42 +02:00
Robinson 19d6d6ebaf
Code cleanup 2023-09-21 12:54:07 +02:00
Robinson e6b4cbd386
Added support for sessions 2023-09-21 12:53:47 +02:00
Robinson 3e9109b4c7
Code cleanup 2023-09-21 11:22:36 +02:00
Robinson c8047987b4
removed unnecessary dir 2023-09-19 22:44:48 +02:00
Robinson 851fcfd1fc
Code cleanup 2023-09-17 02:39:00 +02:00
Robinson b2d349d17c
Merge branch 'session' 2023-09-17 02:37:36 +02:00
Robinson f269684ea5
newConnection method reverted back to function, which allows for easier extension of class types 2023-09-17 02:36:45 +02:00
Robinson e32ecda7ac
updated dep 2023-09-15 20:09:29 +02:00
Robinson 5b43d44b22
Initial work on cache 2023-09-15 20:09:09 +02:00
Robinson 67b4443ade
Ensure the aeron context is always closed and the driver is cleaned up, even if it hasn't been started 2023-09-14 13:39:45 +02:00
Robinson 00a1f4c66b
Limit logging messages for RMI when args are long. 2023-09-14 11:47:30 +02:00
Robinson b2b6cfdc10
Removed KotlinLogging (it has a niche usage that did not apply) 2023-09-13 17:04:25 +02:00
Robinson 560b5bc743
Fixed issue with trace logging + RMI when arguments were large 2023-09-13 16:01:54 +02:00
Robinson 5021eb5136
made read accessible 2023-09-13 16:01:32 +02:00
Robinson 81380fe633
Wrapped logger.debug/trace into if statements to prevent the JVM from creating unnecessary lambdas 2023-09-13 16:01:14 +02:00
Robinson d895e04af5
Fixed issues with streaming for RMI and added another streaming test 2023-09-13 15:57:54 +02:00
Robinson 3fff69757c
Only notify exceptions when message-send-during-close, when we did not explicitly close the connection 2023-09-13 13:52:45 +02:00
Robinson 8e9e0441ed
Fixed issue with with RMI sync/async. 2023-09-13 13:49:13 +02:00
Robinson 3abbdf8825
removed stacktrace output 2023-09-08 14:18:48 +02:00
Robinson 3d8c5275ac
Better error details during connect phase 2023-09-08 14:18:35 +02:00
Robinson c69512eda4
pollerClosedLatch is now only created once we've fully started (prevent blocking forever when shutting down) 2023-09-08 13:21:12 +02:00
Robinson dafcc97eac
cleanStackTrace() now returns itself. 2023-09-08 13:20:24 +02:00
Robinson 78ae3a38e4
Added unit test for closing endpoint while they haven't fully started 2023-09-08 13:19:56 +02:00
Robinson f9b30012b1
RMI fix/cleanup 2023-09-08 02:49:52 +02:00
Robinson e3a565f291
Fixed issues with streaming (it MUST be the aeron thread) 2023-09-08 02:49:35 +02:00
Robinson 50f212b834
updated deps 2023-09-07 18:36:03 +02:00
Robinson d772088eed
Streaming data now goes onto its own context instead of on the aeron polling thread 2023-09-07 18:35:58 +02:00
Robinson c2c45b9ffe
updated deps 2023-09-07 18:32:34 +02:00
Robinson 2aef58b507
version 6.10 2023-09-07 18:15:20 +02:00
Robinson daf289c7b7
updated deps 2023-09-07 18:15:10 +02:00
Robinson df11e40222
Moved all `app` non-unit test code into the app package 2023-09-07 18:08:39 +02:00
Robinson fa03be5e89
various steps to optimize RMI calls (they are ~1.5x as slow as standard message passing 2023-09-07 18:08:22 +02:00
Robinson 56a42e5b7f
comments 2023-09-07 17:26:37 +02:00
Robinson 2e8382eb2f
removed debug code 2023-09-07 17:23:27 +02:00
Robinson a85c647598
Updated collections to use LongMap 2023-09-07 12:24:10 +02:00
Robinson 8428d9899d
Enable to dynamically enable IPC when explicitly called 2023-09-07 12:23:47 +02:00
Robinson 9d0d8efdc0
Initial value of threadID is 0, so we don't have to initialize the poller in order to close it 2023-09-07 12:23:23 +02:00
Robinson ba4df9b33b
Comments 2023-09-07 10:36:35 +02:00
Robinson 94b5226a5a
Fixed issues with RMI not throwing exceptions properly 2023-09-07 10:36:22 +02:00
Robinson 1bb052fed4
comments fixed 2023-09-07 01:02:17 +02:00
Robinson 63dd14015c
Converted to using threads instead of coroutines 2023-09-07 01:01:55 +02:00
Robinson 0173ef7b91
GC performance optimization 2023-09-07 01:01:36 +02:00
Robinson e11287b31e
Converted to executors. 2023-09-07 01:01:22 +02:00
Robinson 94ae22716d
Fixed issues with heap garbage generation and performance (suspend is better than blocking, but only with short execution stacks) 2023-09-07 01:00:53 +02:00
Robinson 7ac284bc1b
Code cleanup 2023-09-07 00:59:57 +02:00
Robinson 9e20a20bbb
Streaming data now supports random placement 2023-09-07 00:45:40 +02:00
Robinson cbb5038eb6
code cleanup 2023-09-06 16:44:40 +02:00
Robinson 9b1650ae31
Code cleanup 2023-09-06 16:24:45 +02:00
Robinson 2a485bd097
code cleanup 2023-09-06 16:20:23 +02:00
Robinson c5b9691bb1
Moved dispatcher to EventDispatcher 2023-09-06 16:20:15 +02:00
Robinson 54eab9d6c8
code cleanup 2023-09-06 16:19:40 +02:00
Robinson 0bd725b2d8
Removed withKryo{} lambda (was causing heap issues) 2023-09-06 16:17:36 +02:00
Robinson b30b024849
Added readKryos for streaming 2023-09-06 16:17:03 +02:00
Robinson 464fbadbd1
Removed coroutine trampoline from JPMS 2023-09-06 12:05:55 +02:00
Robinson 26e6da555b
Now catch and watch `Throwable` instead of just `Exception` 2023-09-05 23:39:40 +02:00
Robinson ae5a48b309
Added back a/syncSuspend 2023-09-05 23:38:53 +02:00
Robinson 48f1555ace
Removed unnecessary Suspend Trampoline 2023-09-05 23:38:32 +02:00
Robinson 3704ae25e7
RmiUtils now accepts `Throwable` instead of `Exception` 2023-09-05 23:38:13 +02:00
Robinson 7c326d180c
Converted the RMI response manager to use blocking instead of suspending calls. 2023-09-05 12:59:49 +02:00
Robinson 3e9a8f9c74
Moved ping to the connection object 2023-09-05 12:58:38 +02:00
Robinson effed36faf
Server Handshake has its own dispatcher, and its in the HandshakePollers object 2023-09-05 12:58:21 +02:00
Robinson 1b2487daec
Error notifications have their own dispatcher now (and it's in the ListenerManager) 2023-09-05 12:57:41 +02:00
Robinson d64a4bb1e1
Moved ping() to the connection object 2023-09-05 12:57:16 +02:00
Robinson 769bad6aac
tweaked function names and timeouts 2023-09-05 12:56:14 +02:00
Robinson 2d061220f5
Cleaned up how errors are managed 2023-09-05 12:55:58 +02:00
Robinson 8b62dbb063
Removed more coroutine, simplified methods 2023-09-04 14:23:06 +02:00
Robinson e185f496ec
Removed coroutines/suspending calls 2023-09-04 00:48:00 +02:00
Robinson 6291e1aa77
ResponseManager uses its own, internal dispatcher for events 2023-09-04 00:01:27 +02:00
Robinson e7999d3095
WIP - removing heap allocations 2023-09-03 21:17:37 +02:00
Robinson ac2cf56fb9
code cleanup 2023-09-03 21:10:25 +02:00
Robinson 4d2ee10c02
fixed error in logic for unit test 2023-09-03 21:10:16 +02:00
Robinson 620e74a506
Added package-info.java 2023-09-03 21:09:54 +02:00
Robinson f631dea046
If reconnect is called on a client WITHOUT being first closed, it will close first. 2023-08-30 12:02:04 +02:00
Robinson 364b29fd0c
version 6.9.1 2023-08-21 19:53:10 +02:00
Robinson b639ec1372
UDP frame size information moved to startup. 2023-08-21 19:52:19 +02:00
Robinson 0747802f0d
updated deps, version 6.9 2023-08-21 02:20:41 +02:00
Robinson 87173af0b7
version 6.8 2023-08-20 14:28:12 +02:00
Robinson 6df290cfd3
updated deps 2023-08-20 13:56:44 +02:00
Robinson bdfc293167
updated gradle 2023-08-20 13:55:11 +02:00
Robinson c95e811fde
version 6.7 2023-08-11 16:23:49 -06:00
Robinson c856c23e3c
Server connections are checked for isConnected() status during poll events 2023-08-11 10:02:57 -06:00
Robinson b39db65027
code polish 2023-08-11 09:57:38 -06:00
Robinson e8724ea4c5
Client connections are checked for isConnected() status during poll events 2023-08-11 09:57:24 -06:00
Robinson 95b1b44890
Only set the send/recv buffer sizes if they have been configured 2023-08-10 23:34:27 -06:00
Robinson 8e7c47abcc
code cleanup 2023-08-10 20:11:23 -06:00
Robinson 96f5406ae6
More careful checks when closing endpoints during restart
code polish
2023-08-10 20:05:49 -06:00
Robinson ad9771263c
driver endpoint list is now concurrent 2023-08-10 20:04:23 -06:00
Robinson 466363901c
code cleanup 2023-08-10 20:03:06 -06:00
Robinson 77d56b8804
Direct access to critical error now instead of proxy 2023-08-10 20:02:38 -06:00
Robinson a36947af5b
updated deps 2023-08-09 22:35:41 -06:00
Robinson 91aed612cc
updated license 2023-08-09 22:35:30 -06:00
Robinson b8a6f5436d
Updated API for unittests 2023-08-09 22:35:15 -06:00
Robinson 50ab7fc72f
config.id -> mediaDriverId() 2023-08-09 22:13:55 -06:00
Robinson def935214f
comment cleanup 2023-08-09 22:13:39 -06:00
Robinson 07e1da3660
Tweaked how waiting for close works 2023-08-09 22:13:12 -06:00
Robinson 2b5e943369
can optionally notifyDisconnect when closing a connection 2023-08-09 22:12:52 -06:00
Robinson 1ded010b89
code cleanup 2023-08-09 22:12:29 -06:00
Robinson 19b36bde9f
driver.start/close are now reentrant 2023-08-09 22:12:18 -06:00
Robinson 90d218637c
Cleaned up how new aeron drivers are created 2023-08-09 22:11:57 -06:00
Robinson 96cd987238
config.id -> mediaDriverId() 2023-08-09 22:09:44 -06:00
Robinson 4d73d4802c
cleaned up imports 2023-08-09 22:06:22 -06:00
Robinson e2b5f522e0
AddError is no longer suspending 2023-08-09 21:47:29 -06:00
Robinson ce311fea86
Added more support for criticalDriverErrors 2023-08-09 21:47:06 -06:00
Robinson d9bac748f8
added endpoint to inUse() 2023-08-09 21:45:54 -06:00
Robinson 6e76160c83
changed config.id -> mediaDriverId() 2023-08-09 21:45:28 -06:00
Robinson 836c8abce6
Cleaned up/tweaked endpoint.close() 2023-08-09 21:35:40 -06:00
Robinson 3852677feb
connect event dispatch check only redispatches when it's ON the EDT, but NOT in the correct one 2023-08-09 21:31:00 -06:00
Robinson e9f7172b62
code polish for event poller 2023-08-09 21:30:17 -06:00
Robinson 4c3135028a
driver.close method cleanup 2023-08-09 21:29:28 -06:00
Robinson a7533d2c91
closed check is now volatile 2023-08-09 21:28:03 -06:00
Robinson db385d0c1a
inUse check now uses the endpoint for extra checks 2023-08-09 21:23:52 -06:00
Robinson 28d170c25c
Added support for detecting critical driver errors 2023-08-09 21:18:47 -06:00
Robinson 3dcd2af495
Moved aeron.send() logic to the driver 2023-08-09 21:17:10 -06:00
Robinson 9fcbabd061
cleaned up logging 2023-08-09 21:10:41 -06:00
Robinson eaafc0f0c4
reset the endpoint config (not the initial config) when resetting. 2023-08-09 16:37:09 -06:00
Robinson 9a30c031ef
Added extra checks when adding pub/sub for when there is an ERRORED state 2023-08-07 22:30:53 -06:00
Robinson 296c600245
Added more detailed info when reconnecing 2023-08-07 19:56:38 -06:00
Robinson 8aa919b28a
simplified connect redispatch logic 2023-08-07 19:56:14 -06:00
Robinson 59bc934dc1
moved checks to earlier in the connect process 2023-08-07 19:55:51 -06:00
Robinson 4d2de085a5
Added data success checks when streaming messages.
Expanded exceptions when thrown
2023-08-07 19:54:49 -06:00
Robinson 6dc7e6bc41
If we close the event poller WHILE ON the event poller, re-dispatch the close event to the CLOSE dispatch 2023-08-07 19:53:59 -06:00
Robinson 08d58fd6fd
Fixed issues with recursive aeron directory name 2023-08-07 00:09:14 -06:00
Robinson ac42a8be7e
updated deps 2023-08-06 01:11:14 -06:00
Robinson 342abd495d
updated deps 2023-08-06 01:00:29 -06:00
Robinson 1c8b9d5023
removed upstream dependency (no longer needed) 2023-08-05 18:41:39 -06:00
Robinson b7f4a09f46
Updated classutils 2023-08-05 13:24:29 -06:00
Robinson ae08ff2c2f
Updated classutils 2023-08-05 13:24:21 -06:00
Robinson 72b4c93206
Removed moshi, updated deps 2023-08-04 23:32:44 -06:00
Robinson 00dffa78e0
Updated for new config project 2023-08-04 23:32:16 -06:00
Robinson 4a80c2c0b8
Reconnect now can have a specified timeout 2023-08-04 23:32:00 -06:00
Robinson 16c8386ae1
Updated API for collections 2023-08-04 23:31:44 -06:00
Robinson 2e904b8ac5
Tweaks for testing performance 2023-07-24 02:03:04 +02:00
Robinson 53cd6ac382
Tweaks for testing performance 2023-07-24 02:02:28 +02:00
Robinson e5786550a6
Updated version 2023-07-24 02:00:03 +02:00
Robinson 8da5215455
enhanced the basic performance test tool 2023-07-24 01:43:21 +02:00
Robinson 3016618b1c
Added support for also changing the aeron driver idle strategies 2023-07-24 01:42:27 +02:00
Robinson 57480735c3
By default, create dev/shm for macos (ram drive). Windows still uses the disk. 2023-07-23 23:36:36 +02:00
Robinson 15c7fb2a3d
Code cleanup 2023-07-23 23:02:29 +02:00
Robinson ccf7a37d3c
Commented out unnecessary code 2023-07-23 16:05:07 +02:00
Robinson fa04185234
Fixed equals 2023-07-23 15:49:25 +02:00
Robinson a140c844db
Code cleanup 2023-07-23 13:41:29 +02:00
Robinson 7f6550f1c1
Now use defaults for idle strategies 2023-07-23 13:40:42 +02:00
Robinson 6754e35c61
Code cleanup 2023-07-23 13:40:16 +02:00
Robinson 06b5f30948
Only check if a connection is closed now. We now wait for pub+sub to be "connected" before continuing to build the connection object (so it will always be in the connected state) 2023-07-23 13:39:27 +02:00
Robinson ad3fdfc64d
Updated text names for idle strategies 2023-07-23 13:30:38 +02:00
Robinson daec762e30
More specific errors when connection is closed during poll event 2023-07-23 01:20:16 +02:00
Robinson 949a863aca
Fixed instance assignment 2023-07-23 01:17:37 +02:00
Robinson a087dfa9bd
Faster startup when aeron is already running and we force-allow a driver to be running on startup (usually we don't want this) 2023-07-23 01:17:09 +02:00
Robinson 936a5e2d67
Added ability for subscription to wait for a publication to connect 2023-07-23 01:16:23 +02:00
Robinson 2d8956c78c
Client waits for server publication to connect before continuing. 2023-07-22 14:18:38 +02:00
Robinson 781d530294
added comments 2023-07-21 22:46:41 +02:00
Robinson ed2ddb239d
Tweaked aeron idle strategies 2023-07-21 22:46:30 +02:00
Robinson ee558e666d
Added additional idle strategies 2023-07-21 21:16:49 +02:00
Robinson ed89b634a2
Added comments 2023-07-21 21:16:37 +02:00
Robinson 4e232aa18e
Fixed unit tests shutdown lifecycle ordering 2023-07-21 00:20:47 +02:00
Robinson c4129f25fa
Updated test app 2023-07-21 00:20:23 +02:00
Robinson 2620a06409
Cleaned up how kryo's are used
Changed idleStrategy
StreamingManager no longer copies bytes (it just uses a pooled kryo instance)
2023-07-21 00:19:31 +02:00
Robinson 7ed474111a
Code cleanup 2023-07-20 22:31:27 +02:00
Robinson 081ee42a2e
removed dead code 2023-07-20 20:41:33 +02:00
Robinson 916ddb857f
fixed comment typo 2023-07-20 20:41:23 +02:00
Robinson 2c0680b513
Simplified setting aeron initial window length 2023-07-20 20:41:13 +02:00
Robinson 0e37689c2c
better logs when retrying the connect sequence 2023-07-20 20:40:43 +02:00
Robinson 7bd653db2a
Better IPC checking 2023-07-20 20:40:17 +02:00
Robinson e2a4887a19
Changed order of cleanup when done with handshake 2023-07-20 20:39:58 +02:00
Robinson 0e3cc803b2
More detailed logging when in debug mode 2023-07-20 20:39:26 +02:00
Robinson 80d77f2f51
Better lock-step and checks when closing an endpoint 2023-07-20 20:39:12 +02:00
Robinson d787045149
Updated version 2023-07-16 14:58:01 +02:00
Robinson 94048cfe8f
Added file transport to streaming manager 2023-07-16 14:57:04 +02:00
Robinson bb026f377b
WIP compression/crypto 2023-07-15 13:12:59 +02:00
Robinson 411a4c54b8
Better error checking. added kryo-exception checking/failures for unittests 2023-07-15 13:12:25 +02:00
Robinson c4eda86bfe
Optimized how we send data (we use our own stream/block data structures for fragmentation/reassembly. 2023-07-15 13:11:50 +02:00
Robinson 93a7c9008d
Code cleanup and fixed issues when sending non-perfect multiples of our data limit. 2023-07-15 13:07:17 +02:00
Robinson 85d716e572
Changed which data structure is evaluated when saving data 2023-07-15 13:05:57 +02:00
Robinson 5583948961
Added AeronWriter size initialization 2023-07-15 13:05:19 +02:00
Robinson 307b8f558f
Updated comments/dependencies 2023-07-14 13:47:59 +02:00
Robinson 2f8c78ddee
Added ipcMTU to aeron config (it will be the same as the network MTU). This must be the same value, since our internal read/write serialization buffers) 2023-07-14 13:39:08 +02:00
Robinson 215ed20056
Changed wording of chunk -> block 2023-07-14 13:33:36 +02:00
Robinson 290c5bd768
Cleaned up comments 2023-07-12 14:20:04 +02:00
Robinson 09748326c9
Cleaned up crypto management, removed dead code 2023-07-12 14:19:54 +02:00
Robinson 6bf870bd7b
Split kryo TYPES into read/write types, so usage is very clear. Now use a kryo pool for concurrent serialization 2023-07-12 14:08:46 +02:00
Robinson d3c3bf50d6
Changed applicationId -> appId 2023-07-12 14:04:32 +02:00
Robinson 2f7a365f75
Moved `errorCodeName` into the driver 2023-07-11 11:50:48 +02:00
Robinson 90830128e6
Removed dead code 2023-07-11 11:50:32 +02:00
Robinson f1ebd076bf
Removed dependency on aeron-aal (which was only for samples) 2023-07-11 09:48:26 +02:00
Robinson 87b65d061a
Now supports JPMS (kotlin-only 9+ projects must use a workaround) 2023-07-11 00:27:39 +02:00
Robinson c4ddfe8675
Fixed unnecessary non-null assertions 2023-07-11 00:23:08 +02:00
Robinson 990652288e
Code cleanup 2023-07-11 00:12:09 +02:00
Robinson 4797b7e816
Added port1/2 settings to server + client.
Fixed relevant unit tests
2023-07-11 00:11:58 +02:00
Robinson ebad4d234b
Fixed driver liveliness checks 2023-07-11 00:02:40 +02:00
Robinson cba66a6959
Removed SigInt catch. It should be managed by the application, not the library. 2023-07-05 12:53:21 +02:00
Robinson 897db748e7
Removed dead code 2023-07-05 12:52:36 +02:00
Robinson ce6ffec197
When an endpoint restarts too quickly, wait a more appropriate timeout 2023-07-05 12:52:23 +02:00
Robinson a8903b2382
Cleaned up close API 2023-07-04 01:00:01 +02:00
Robinson 74066060d7
Fixed close ambiguity 2023-07-03 22:02:02 +02:00
Robinson 739ad30987
Exposed direct commands to delete the aeron directory 2023-07-03 21:44:09 +02:00
Robinson d43be07874
More careful checks when shutting down (and ensuring stopped drivers during unit tests) 2023-07-03 21:42:16 +02:00
Robinson f4c01d6f94
Expose the endpoint config. 2023-07-03 21:41:06 +02:00
Robinson b4a57c9525
ListenerManager now uses volatile arrays instead of atomic (still follows single-writer-principle) 2023-07-03 21:40:07 +02:00
Robinson 2b4ba1347e
Connection handshake timeouts are more standardized to nanoseconds 2023-07-03 19:18:34 +02:00
Robinson 23d4ea4609
Cleaned up deleting log files on a bad handshake 2023-07-03 14:59:11 +02:00
Robinson effc88feb7
Registered close to SigInt 2023-07-03 14:58:46 +02:00
Robinson a4e2e714c4
made event access more east to understand 2023-07-03 14:58:29 +02:00
Robinson 7b7910d078
Better try/catch around handshake logic 2023-07-03 11:57:26 +02:00
Robinson 7a39044df0
Fixed memory leak when shutting down 2023-07-03 11:23:42 +02:00
Robinson ee296394de
Fixed issues when deserializing messages during the HANDSHAKE, where we would run out of memory (instantly delete the image file representing the bad connection). 2023-07-03 10:46:18 +02:00
Robinson 0ae5b5f927
Updated deps 2023-07-03 01:47:39 +02:00
Robinson 2a52c2b4d5
Cleaned up how pub/priv key are initialized/used 2023-07-03 01:47:18 +02:00
Robinson e85025c199
Better log messages 2023-07-03 00:54:57 +02:00
Robinson eeeeed81aa
Better double check when starting up to see if aeron is already running 2023-07-03 00:54:44 +02:00
Robinson b69c199bf4
updated test for kotlin val get/set 2023-07-02 22:03:23 +02:00
Robinson 264081b38b
updated license 2023-07-02 22:00:32 +02:00
Robinson edada596f0
public/private keys + salt use kotlin val get/set now (cleaner API) 2023-07-02 22:00:11 +02:00
Robinson 6220ec617b
Better toString() methods for the unique ID of the client/server 2023-07-02 21:59:45 +02:00
Robinson e403e1d6e1
Now use the client public-key as the client ID for the connection (to determine/know exactly which connection belongs to what client) 2023-07-02 21:56:46 +02:00
Robinson 87e790dcaf
Added support for publication cache during handshake. 2023-07-01 22:51:53 +02:00
Robinson 0848badc69
IP address/bind check ignored for IPC connections 2023-07-01 22:51:20 +02:00
Robinson 0e13da73c9
fixed typo 2023-07-01 22:50:22 +02:00
Robinson b9fc246af1
Code cleanup 2023-07-01 13:44:02 +02:00
Robinson eb0bde3354
Removed unnecessary buffer flush on error 2023-07-01 13:31:50 +02:00
Robinson c35b2c3115
Fixed merge conflicts 2023-07-01 13:26:06 +02:00
Robinson 598e06e9d3
Fixed merge conflicts 2023-07-01 13:20:14 +02:00
Robinson 7d331f8a1d
Fixed unit test 2023-07-01 13:18:39 +02:00
Robinson 141a39544a
part 2, moved IPC/UDP handshake ids to config 2023-07-01 11:43:55 +02:00
Robinson 6020f06661
code cleanup 2023-07-01 11:43:20 +02:00
Robinson a42c9ad940
Better log message 2023-07-01 11:42:54 +02:00
Robinson d1184be6ab
More structured log messages and shutdown is more orderly 2023-07-01 11:42:29 +02:00
Robinson cce3957089
Moved IPC/UDP handshake stream ID to config 2023-07-01 11:41:47 +02:00
Robinson 55737c41c3
Code cleanup 2023-07-01 11:41:10 +02:00
Robinson ace2ac453b
When adding a pub/sub, guarantee that it is active and bound. 2023-07-01 11:37:09 +02:00
Robinson a41a6c6d62
Added comments 2023-07-01 11:28:34 +02:00
Robinson 12977bb046
wait longer if the system is trying to start too quickly 2023-07-01 11:28:22 +02:00
Robinson 6202015b08
Cleaned up newException stack traces 2023-06-30 00:06:53 +02:00
Robinson 9f3c265ee1
Flush the read buffer when kryo encounters an error reading data 2023-06-30 00:04:29 +02:00
Robinson 2a1c303c6a
Moved localAddressString logic the Driver 2023-06-29 23:51:56 +02:00
Robinson 0cc8828ce8
Better log message for what port is used on the server 2023-06-29 21:50:49 +02:00
Robinson 7cbfa679b5
More expressive error logs when a pub/sub cannot be created 2023-06-29 21:50:28 +02:00
Robinson daa0d14e2b
Endpoint RuntimeShutdown hook is in a try/catch now 2023-06-29 20:57:01 +02:00
Robinson ae67bb4899
Client/Server isRunning checks will COPY the config instead, so that we don't "hack" the configs to make sure they continue working as expected 2023-06-29 20:56:43 +02:00
Robinson c0e8cbf28f
Added retry limit of 0 to try forever 2023-06-29 20:16:42 +02:00
Robinson a6f1664740
Updated license 2023-06-29 20:02:11 +02:00
Robinson 7c0283b775
Removed dead code 2023-06-29 20:01:38 +02:00
Robinson 69acc26c94
Updated build deps 2023-06-29 19:59:21 +02:00
Robinson fa023204ff
updated build config 2023-06-29 19:58:44 +02:00
Robinson 95372a0e4c
updated module-info 2023-06-29 19:58:31 +02:00
Robinson 835ae99e9c
Code cleanup 2023-06-29 19:53:31 +02:00
Robinson 7281cdb9f4
Added lingerNS to ensureStopped 2023-06-29 19:28:08 +02:00
Robinson 8ed44904d1
updated license 2023-06-29 19:27:35 +02:00
Robinson c66f977963
Endpoints will auto-close during JVM shutdown 2023-06-29 19:27:25 +02:00
Robinson 06f26187ce
code cleanup 2023-06-29 19:26:45 +02:00
Robinson d4c70c4c1f
Updated unit tests to more aggressively check if aeron is running on startup 2023-06-29 19:26:27 +02:00
Robinson dec10ab4bc
OnError callbacks are now suspending 2023-06-29 12:12:29 +02:00
Robinson 15f93cf1b0
AeronOutput is not-final 2023-06-29 12:12:14 +02:00
Robinson 5ec2abfc9b
Added better support for streaming large amounts of data (uses temp files if it's too much data) 2023-06-29 12:11:17 +02:00
Robinson 53939470b9
Added the required application ID. Added ability to override client-listen-port, permit allowing to run shared instances of the aeron driver across processes. 2023-06-29 01:32:04 +02:00
Robinson 510fc85f2c
Removed streaming kryo and temp kryo 2023-06-28 20:31:08 +02:00
Robinson ecbbd55ff6
Fixed unit tests 2023-06-28 15:12:55 +02:00
Robinson 05ba2c8132
Fixed issues with wrong subscription listening for DONE_ACK 2023-06-28 15:12:40 +02:00
Robinson 48cece9202
Code cleanup 2023-06-28 15:12:10 +02:00
Robinson bd289c7ce6
Converted to use exclusive publications, cleaned up wait-to-connect logic, removed deletePub/Sub (that's not recommended for production) 2023-06-28 15:11:11 +02:00
Robinson cdb90b1809
Refactored serialization (it is more explicit now) 2023-06-27 01:25:56 +02:00
Robinson 55604c679c
Moved port from configuration file to API call for connect() and bind() 2023-06-26 19:28:55 +02:00
Robinson bdad03111c
Updated build deps 2023-06-26 02:23:50 +02:00
Robinson bd14234bc7
Updated license 2023-06-26 02:23:06 +02:00
Robinson 8983c2b8f3
Updated readme 2023-06-26 02:22:46 +02:00
Robinson a02c737c6c
Updated inet serializer location 2023-06-26 02:11:43 +02:00
Robinson 58c8d87da4
Initial implementation of JMPS bypass for the TransportPoller 2023-06-26 02:11:10 +02:00
Robinson 43c4b6c742
Fixed issues when shutting down before starting up 2023-06-26 00:37:33 +02:00
Robinson 6c4cb55a2e
updated license 2023-06-25 17:27:29 +02:00
Robinson 2eeed92c97
updated logging 2023-06-25 17:27:12 +02:00
Robinson 231ab230ba
Updated disconnect api 2023-06-25 17:26:05 +02:00
Robinson a81e5316c7
updated unit tests 2023-06-25 17:25:29 +02:00
Robinson d63b8a6514
changed logger 2023-06-25 17:25:13 +02:00
Robinson 3897becef1
Addec copy(), changed the defaultMessageCoroutineScope to be 'Default' instead of IO. The defaultNetworkEventPoll is now in the event poller 2023-06-25 17:25:05 +02:00
Robinson 078cf36e22
Permit external usage of closing everything when shutting down 2023-06-25 17:23:44 +02:00
Robinson 3a07b6bf86
Reference of existing list is kept before dispatching into new threads 2023-06-25 17:22:59 +02:00
Robinson 9a3e49bca4
Code cleanup 2023-06-25 17:21:25 +02:00
Robinson 3a9e9fea71
fixed issues with event poller lifecycle 2023-06-25 17:20:55 +02:00
Robinson 88f082097a
code cleanup 2023-06-25 12:21:21 +02:00
Robinson 4b511b2615
Cleaned up comments and logging 2023-06-25 12:13:52 +02:00
Robinson 119870bdc8
Added more comments, events now run on specific threads 2023-06-25 12:12:53 +02:00
Robinson 8e30883ebc
Code polish/cleanup 2023-06-25 12:05:41 +02:00
Robinson c9e71c56b1
Code polish 2023-06-25 12:02:04 +02:00
Robinson e02ef7d7e8
Handshake cleanup 2023-06-25 12:01:37 +02:00
Robinson 03974514c5
Code cleanup, added UUID support 2023-06-25 11:52:19 +02:00
Robinson ff5d1ed430
Added logging 2023-06-25 11:52:01 +02:00
Robinson 80b4f2af48
Code cleanup 2023-06-24 12:23:07 +02:00
Robinson 3c02c28162
Added uuid, cleaned up logic 2023-06-24 12:21:57 +02:00
Robinson f95b1f63ca
Updated copyright/comments 2023-06-24 02:06:17 +02:00
Robinson 422e983e85
Added comments 2023-06-24 02:06:03 +02:00
Robinson 115e63edf5
Added more exception types 2023-06-22 11:47:02 +02:00
Robinson 76c644b573
Added client UUID support 2023-06-22 11:46:30 +02:00
Robinson f991182bbd
If a connection is manually closed, don't wait for aeron timeouts 2023-06-22 11:43:50 +02:00
Robinson 27696a0929
Added reserved class serialization registrations 2023-06-20 16:28:12 +02:00
Robinson 87a6fefe69
Code/logging cleanup 2023-06-19 14:03:18 +02:00
Robinson 1556cbe10d
Added method to easily reverse pub/sub info and added log output 2023-06-19 13:54:56 +02:00
Robinson 3d37a2267f
added more exception types 2023-06-19 13:53:28 +02:00
Robinson da0db98658
more specific parameter names, better toString() info 2023-06-19 12:19:04 +02:00
Robinson 21e2504719
Added more exception types 2023-06-19 12:17:49 +02:00
Robinson 78208fcecb
waitForThreads will wait the default timeout 2023-06-18 19:41:57 +02:00
Robinson 26e2d4a52c
atomic value lazy setting 2023-06-18 19:41:22 +02:00
Robinson bd7bb78696
Cleaned up logging 2023-06-18 18:19:23 +02:00
Robinson ddb41762cf
Response manager uses its own coroutineScope now 2023-06-18 18:18:22 +02:00
Robinson 49b9ee98a2
ResponseManager now uses it's own EventDispatcher for events (to prevent potential deadlocks during RMI operations) 2023-06-17 12:16:50 +02:00
Robinson 7e748bd7dc
Reference copies of atomic lists when calling them (in case the list is cleared in a reentrant call 2023-06-16 14:48:19 +02:00
Robinson cf4b61f4de
logic update 2023-06-16 14:19:58 +02:00
Robinson eeed70b2c3
Updated error logs 2023-06-16 14:19:46 +02:00
Robinson 9e3a5b4d16
Updated secure random 2023-06-16 14:18:42 +02:00
Robinson 8ac3def5d3
updated license 2023-06-16 14:15:36 +02:00
Robinson bc0a0e2dcc
Updated unit tests APIs 2023-06-16 14:15:27 +02:00
Robinson 173ad3a691
Updated copyright 2023-06-16 11:18:03 +02:00
Robinson 539722f520
Added support for maxCapacity when calling `setBuffer` 2023-06-16 11:16:00 +02:00
Robinson 1b9329b734
updated license 2023-06-14 23:36:05 +02:00
Robinson 4a800349b6
MultiClient tests now test 80 concurrent client connections 2023-06-14 23:35:44 +02:00
Robinson 5c4b8bc202
Added more unit tests 2023-06-14 23:35:22 +02:00
Robinson 327ac46f42
Cleaned up API 2023-06-14 23:35:03 +02:00
Robinson 4f74b56a13
Cleaned up api and logging 2023-06-14 23:29:15 +02:00
Robinson 4f33369663
Fixed issues and cleaned up aeron media driver connection factories 2023-06-14 23:28:34 +02:00
Robinson d268dde7a3
Removed old driver connection logic 2023-06-14 21:30:43 +02:00
Robinson cda6b53110
Added Aeron pub/sub unit test 2023-06-14 09:31:50 +02:00
Robinson 11247aa2e4
Fixed api usage 2023-06-13 15:48:59 +02:00
Robinson 76c2ab782a
Fixed invalid startup state for pollDispatcher 2023-06-13 15:48:49 +02:00
Robinson ad538645c7
updated function parameters and comments 2023-06-07 11:59:08 +02:00
Robinson 7bde7ac3df
added comments 2023-06-07 11:49:01 +02:00
Robinson 25db1740f8
Added session+stream sub/pub IDs, added comments, fixed issues when exceptions were thrown 2023-06-07 11:48:45 +02:00
Robinson 9047ac386b
Cleaned comments 2023-06-07 11:46:02 +02:00
Robinson ebc7b1cd76
More clear logging/checking when closing the poller 2023-06-07 11:44:52 +02:00
Robinson 6bbab72ca7
Uses SuspendingPool instead of channels (better tested library) for fewer bugs/issues 2023-06-06 00:15:41 +02:00
Robinson 7a99ea67b3
revert ping object changes 2023-06-06 00:11:13 +02:00
Robinson c6b02869d2
update copyright 2023-06-06 00:10:47 +02:00
Robinson 65d5676c7b
Changed defaut connect method 2023-06-06 00:10:20 +02:00
Robinson 65071f08da
Added more edge-cases for unit tests. Removed shutdown timeout 2023-06-06 00:10:05 +02:00
Robinson 2418290fcd
removed shutdown timeout 2023-06-06 00:09:44 +02:00
Robinson b2d5de306f
Proper use of countdown latch instead of object.wait 2023-06-06 00:09:22 +02:00
Robinson b64aae00a5
Fixed closing the endpoint for unit test 2023-06-01 14:49:22 +02:00
Robinson 9a13598d63
Updated for ping tests to always stop endpoints 2023-05-28 22:47:17 +02:00
Robinson dfe9491272
Fixed issues where some PING responses would not be returned (as a result of memory visibility/permanence) 2023-05-28 22:46:25 +02:00
Robinson 698a669d60
Removed dead code, added pingTimeout 2023-05-28 19:19:37 +02:00
Robinson 0aaff26e69
Moved dispatch requirements 2023-05-28 18:44:53 +02:00
Robinson 132a1d8363
Removed IPC sessionID. It's one IPC connection per Aeron Driver. 2023-05-28 18:43:59 +02:00
Robinson e35cf8afe4
Updated unit tests 2023-05-28 18:41:46 +02:00
Robinson b42b456daf
Added initialize state for endpoints 2023-05-28 17:06:01 +02:00
Robinson d8455e1faf
added comments, code cleanup 2023-05-28 17:04:30 +02:00
Robinson cf875832d9
code cleanup 2023-05-28 17:03:54 +02:00
Robinson 167de54114
Flipped order of closing (it's the same order as init) 2023-05-28 17:03:30 +02:00
Robinson e1997eb8cc
getting the aeron log location is now static public. 2023-05-28 17:03:05 +02:00
Robinson 4df378e8d4
Added UUID to endpoint for ID 2023-05-28 17:02:15 +02:00
Robinson b4d4d7e049
Handshaker specific params moved 2023-05-28 16:59:58 +02:00
Robinson edc7e586f1
Extracted Kryo.readBytes() to mark private (what should be private) 2023-05-28 16:59:13 +02:00
Robinson a17dbac0fc
Added trace logging info to the serializer 2023-05-28 16:58:23 +02:00
Robinson 48ef6d543d
Code cleanup 2023-05-28 16:57:06 +02:00
Robinson 23572ea9fd
Added debug event logs to the event dispatcher 2023-05-28 16:56:05 +02:00
Robinson c119981859
Code cleanup 2023-05-28 16:54:37 +02:00
Robinson bb952f99df
Migrated handshaker stuff out of endpoint, and into the "handshaker" 2023-05-28 16:53:56 +02:00
Robinson e724048b4a
cleand imports 2023-05-28 16:49:57 +02:00
Robinson 5d56f37a0b
removed HANDSHAKE_SESSION_ID (these have to be UNIQUE, so this doesn't make sense to use) 2023-05-28 16:49:50 +02:00
Robinson 2dd7aa8bc0
Cleaned up adding/removing pub/sub 2023-05-28 16:49:10 +02:00
Robinson 080e27d6ad
added areAllInstancesClosed() 2023-05-28 16:43:01 +02:00
Robinson 71764755ac
Fixed issues with error reporting when the connection is created. 2023-05-28 16:24:16 +02:00
Robinson 8b7eadc01e
Cleaned up logging for the Event poller 2023-05-26 15:46:11 +02:00
Robinson 4c5cca9b84
cleaned logging 2023-05-26 15:43:18 +02:00
Robinson 5d2e6ac551
Cleaned up server handshakes 2023-05-26 15:41:21 +02:00
Robinson 784d0ecf02
Cleaned up random ID allocator 2023-05-26 15:34:26 +02:00
Robinson 6200c5e887
Changed close to suspending 2023-05-24 11:55:07 +02:00
Robinson 5cf41580fd
Cleaned up logging 2023-05-24 09:28:55 +02:00
Robinson 92d192bd1b
removed logger from EventPoller api 2023-05-24 09:21:39 +02:00
Robinson f40d36c488
Fixed issues with unique aeron directory naming 2023-05-24 09:15:55 +02:00
Robinson c2a5befb09
Added the ability to close the listenerManager 2023-05-24 00:15:04 +02:00
Robinson 1c13c17d1f
updated license 2023-05-08 09:59:44 +02:00
Robinson 2d87e003dc
Fixed issues surrounding the session/stream ID rewrite and connection handshake/state management 2023-05-08 09:58:24 +02:00
Robinson 95d7006c74
Cleaned up stack-trace cleanup method invocation 2023-04-29 00:46:16 +02:00
Robinson e24dbcd0b1
cleaned IPC to mirror UDP connections 2023-04-21 23:56:13 +02:00
Robinson 8f8d6cb82e
Update copyright 2023-04-20 18:20:10 +02:00
Robinson 17e711039e
EventDispatcher is static 2023-04-20 18:18:49 +02:00
Robinson c2d1b85b87
Updated AeronDriver/Context API 2023-04-20 18:17:51 +02:00
Robinson d2550a98e2
updated close signature 2023-04-20 17:58:15 +02:00
Robinson e84be7f96a
Cleanup client reconnect logic 2023-04-20 17:57:14 +02:00
Robinson b2f2077550
Fixed client connection logic (some logic is server always) 2023-04-20 17:55:58 +02:00
Robinson da61b70321
EventDispatcher is static 2023-04-20 17:55:17 +02:00
Robinson a403292ba8
Updated poller 2023-04-20 17:53:02 +02:00
Robinson 2ca87dfcb1
Event dispatcher events 2023-04-20 17:52:21 +02:00
Robinson 74dbdf02b5
Updated Configuration settings for unique directories 2023-04-20 17:51:07 +02:00
Robinson ecdde53a3b
update AeronPoller close signature 2023-04-20 17:48:27 +02:00
Robinson 838b2d7ee3
Updated event polling for aeron network events 2023-04-20 17:47:45 +02:00
Robinson 8238dfffbd
updated collection library 2023-04-20 17:46:41 +02:00
Robinson f7de6f4c9d
EventDispatcher is now static 2023-04-20 17:45:34 +02:00
Robinson 0dcd635f6d
connection filtering is only for the server now 2023-03-17 15:07:45 +01:00
Robinson d3b0964b61
Cleaned up unit tests 2023-03-17 15:05:59 +01:00
Robinson 5533d6f794
updated copyright in unit tests 2023-03-17 15:00:00 +01:00
Robinson 517f14f816
logger is now private 2023-03-17 14:59:04 +01:00
Robinson 47c4ce1cd1
Added DisconnectMessage to serialization 2023-03-17 14:58:43 +01:00
Robinson 460840fad3
PING cancelRequest now uses send(), so that events are processed in order (instead of concurrently) 2023-03-17 14:58:04 +01:00
Robinson eb0e59b7ba
runBlocking is now used for event registrations 2023-03-17 14:31:57 +01:00
Robinson 81e2965d10
optimized errorHandler 2023-03-17 14:06:32 +01:00
Robinson 7261a1dcfe
Cleaned up how the media driver connections work. They are also suspending instead of blocking 2023-03-17 14:05:50 +01:00
Robinson 0b7aa96ede
aeron dir is now auto-set to be absolute 2023-03-17 14:02:42 +01:00
Robinson 11d7645f7a
updated copyright 2023-03-17 13:58:52 +01:00
Robinson a8f28d2814
kotlin map API + License header 2023-03-10 21:28:40 -06:00
Robinson 24f2246016
Errors will ALWAYS log, and will log only 1 time. 2023-03-05 17:20:38 +01:00
Robinson 1bdb5d546d
Reconnects during a disconnect are now re-dispatched appropriately 2023-03-02 19:46:08 +01:00
Robinson 559880b71c
Server disconnects do not call connection.connect (only clients can do this). Removed associated logic 2023-03-02 19:45:41 +01:00
Robinson 8f81243c25
MOved event dispatch to own class (as it is not user configurable) 2023-03-02 19:44:54 +01:00
Robinson 062b8a76ae
updated API 2023-03-02 19:44:06 +01:00
Robinson 96b5bcf905
moved runBlocking to invoking method 2023-03-02 19:43:24 +01:00
Robinson 9d04c3acb1
added new event dispatcher 2023-03-02 19:42:42 +01:00
Robinson cbbef7f48a
Updated error handler usage, updated to use new event dispatch system, added more logging info 2023-03-02 19:42:22 +01:00
Robinson 408b41470e
updated API 2023-03-02 19:41:03 +01:00
Robinson 7ec23c59fa
Updated aeron event poller 2023-03-02 19:40:42 +01:00
Robinson fc30c16758
Removed suspending methods (it was not necessary) 2023-03-02 19:38:21 +01:00
Robinson 2455f08b9a
updated API 2023-03-02 19:37:51 +01:00
Robinson 07219d058c
Moved network EventPoller to aeron package 2023-03-01 22:41:08 +01:00
Robinson 5833292975
Updated dispatch API 2023-03-01 22:40:12 +01:00
Robinson 66147174cc
Updated comments 2023-03-01 22:39:38 +01:00
Robinson b29e6e583c
Fixed pingManager access 2023-03-01 22:39:29 +01:00
Robinson 8694f97217
Now using the NetworkEventPoller for polling ALL aeron subscriptions within the JVM 2023-03-01 22:39:03 +01:00
Robinson ee3d0c7dda
Updated API 2023-03-01 22:36:41 +01:00
Robinson 3856483424
Added copyright 2023-03-01 22:36:27 +01:00
Robinson 6c846775eb
updated license 2023-03-01 20:34:01 +01:00
Robinson 4a26b613ff
Changed class name test to serr 2023-03-01 12:50:06 +01:00
Robinson 9b1c594abb
Don't log (and trigger the "already shown tempFS tips) message if we are the NOP.LOGGER (which is when we check if we are running, it's the NOP logger.)" 2023-03-01 12:49:45 +01:00
Robinson b4c7203c71
Close the network Event dispatcher when done 2023-03-01 12:49:10 +01:00
Robinson e0e8d06eaf
Added notifyError(global) and added array.remove 2023-03-01 12:46:12 +01:00
Robinson f1e6df094d
Added another type for equality/hash. Fixed issues with event/message dispatch 2023-03-01 00:24:44 +01:00
Robinson 8cdf5781e2
code warnings 2023-03-01 00:22:46 +01:00
Robinson b5593f52c8
Added RMI remote client/server test 2023-03-01 00:22:36 +01:00
Robinson d6ff32a05d
Updated copyright + log level 2023-03-01 00:22:11 +01:00
Robinson 3f48fcc313
Added defaults + checks for ResponseManager 2023-03-01 00:21:12 +01:00
Robinson 7ee5dde112
Added RMI Response manager to unit tests 2023-03-01 00:20:39 +01:00
Robinson a7a606f1be
API/Code cleanup 2023-02-27 11:31:34 +01:00
Robinson 340dc4a36c
Cleaned API and logging 2023-02-27 11:09:33 +01:00
Robinson e38ff52c08
Updated pub/sub API 2023-02-26 18:18:19 +01:00
Robinson 912a2f4c2a
Updated settings store for new API 2023-02-26 18:17:30 +01:00
Robinson c8af0ba33b
Moved add() to companion object 2023-02-25 13:23:29 +01:00
Robinson b65005828c
Use the windows hi-rez timer 2023-02-25 10:21:47 +01:00
Robinson 9969dcd9b8
AeronContext now implements Closable 2023-02-24 20:28:32 +01:00
Robinson 1ab11f7c78
cleaned up log 2023-02-24 16:35:51 +01:00
Robinson 0a11917b17
updated mooTwo 2023-02-24 16:35:00 +01:00
Robinson 17ed752ec1
added support for debugging connections 2023-02-24 16:33:39 +01:00
Robinson 7fd5f4e577
Cleaned up comments 2023-02-23 20:54:30 +01:00
Robinson 389be7b6db
added equals/hashCode 2023-02-23 12:37:00 +01:00
Robinson 0866f1b6b8
Code cleanup 2023-02-21 19:39:51 +01:00
Robinson 6c433a918f
RemoteObject casting can now be inferred 2023-02-21 19:36:57 +01:00
Robinson 63f60a5f96
When closing a connection, we use the writeMutex instead of 'messagesInProgress' 2023-02-21 19:36:25 +01:00
Robinson 1fd9cdd405
Fixed casting issues 2023-02-20 22:59:52 +01:00
Robinson bb820b03b4
Starting the aeron driver ALWAYS happens since the handshake loop, within the try/catch 2023-02-20 20:57:04 +01:00
Robinson a1a869346e
Updated log messages 2023-02-20 19:27:53 +01:00
Robinson 80e2a57d74
Cleaned API usage 2023-02-20 19:26:55 +01:00
Robinson 0753a08b9a
Updated comments and error messages 2023-02-20 19:26:21 +01:00
Robinson a25e17cc1a
added mooTwo 2023-02-16 23:23:47 +01:00
Robinson 8ae3f15f8e
Added support for IPC/UDP term buffer length (with the default the same as it was previously) 2023-02-16 21:43:45 +01:00
Robinson 951dba3af6
Cleanup 2023-02-16 00:51:23 +01:00
Robinson 3772de7cca
Updated log messages to be consistent between server/client 2023-02-16 00:50:32 +01:00
Robinson b806ff43db
Fixed class visibility 2023-02-15 23:29:02 +01:00
Robinson 024ff53e20
Cleaned up duplicate aeron pub/sub logs 2023-02-15 23:28:45 +01:00
Robinson c57f0564db
Added method parameter names 2023-02-15 23:28:15 +01:00
Robinson ba3ea72369
Cleaned up comments 2023-02-15 23:27:11 +01:00
Robinson bdb1386504
Cleaned up duplicate aeron pub/sub logs 2023-02-15 23:26:53 +01:00
Robinson 230ad249bd
Added ip address to logs for connect/disconnect/timeout/etc 2023-02-15 15:47:02 +01:00
Robinson 3064545645
Send buffer is smaller (1mb) 2023-02-15 15:22:49 +01:00
Robinson 76a96a89ef
Cleaned up stopping/starting the aeron driver 2023-02-15 01:15:51 +01:00
Robinson 73efd8d370
easier code to read 2023-02-15 01:15:05 +01:00
Robinson 9f42800d6a
updated version 2023-02-14 23:47:04 +01:00
Robinson c289f02cb9
code/comment cleanup 2023-02-10 21:21:09 +01:00
Robinson ee25f0d2ce
removed dead code 2023-02-10 21:20:34 +01:00
Robinson 1d30329383
Cleaned stacktraces in the client handshake 2023-02-10 21:20:27 +01:00
Robinson 1f562b880c
Cleaned up allocator constructor 2023-02-10 21:20:13 +01:00
Robinson 47d8956635
Make sure that registrations are rechecked for RMI implementations. 2023-02-09 00:44:57 +01:00
Robinson cdd6c7e7bd
Updated build deps and license 2023-02-08 22:56:56 +01:00
Robinson 488eda3fcc
Refactor serialization initialization and kryo usage.
StreamingManager has better security wrt session IDs
2023-02-08 22:56:23 +01:00
Robinson b1c5b39de7
Fixed logger override initialization order 2023-02-08 22:27:53 +01:00
Robinson 47afe38dd2
transitive for kotlin logging util 2023-02-07 22:23:11 +01:00
Robinson ebd6cd3082
Updated build deps 2023-01-12 11:21:03 +01:00
Robinson f6105a3b11
Updated version 2023-01-05 23:10:31 +01:00
Robinson 489ca884ff
Updated deps 2023-01-05 23:10:13 +01:00
Robinson 95b724c509
Shutdown the client Aeron instance (if it's the only one), if there is any kind of exception thrown during the connection process (not only on retry events) 2023-01-04 23:30:25 +01:00
Robinson f1220c27a8
updated license 2023-01-02 16:16:26 +01:00
Robinson 15245bea21
updated deps 2023-01-02 16:16:18 +01:00
Robinson 24a0a0d427
updated version 2022-12-17 23:49:36 +01:00
Robinson e74f4a4e10
Pub/Sub are now closed when exceptions are thrown 2022-12-17 23:21:44 +01:00
Robinson d408493907
v6.1 2022-12-17 22:24:33 +01:00
Robinson 88121bf0cc
WIP JPMS 2022-12-17 22:24:07 +01:00
Robinson e3519d290f
build update 2022-12-16 00:01:23 +01:00
Robinson a311a47eaf
Added sync/async unit block for thread/concurrent safe sync/async calls 2022-12-16 00:01:11 +01:00
Robinson 48d4ba9b00
Moved idle strategy reset 2022-12-15 23:58:27 +01:00
Robinson ad4073c632
close the sub/pub for a client when it fails to connect (prevent disk space leak) 2022-12-15 23:57:33 +01:00
Robinson a7e520caed
udpated license 2022-12-15 23:56:18 +01:00
Robinson 609cf7e827
updated gradle 2022-12-15 23:55:59 +01:00
Robinson 9689b3a1a7
renamed field 2022-12-15 23:55:40 +01:00
Robinson 688541fadf
Fixed typo 2022-11-21 16:14:21 +01:00
Robinson eb89d8465f
updated build deps 2022-11-21 13:46:28 +01:00
Robinson 714961dd03
Updated dependencies/version 2022-11-16 01:12:35 +01:00
Robinson 6ea4c6477f
Suppress casting when returning kryo 2022-11-16 01:12:17 +01:00
Robinson b94c55cd4f
Fixed compiler warnings 2022-11-15 00:18:18 +01:00
Robinson c918f973b9
Updated gradle 2022-11-14 23:48:04 +01:00
Robinson 7145c23692
Fixed kotlin smart cast 2022-11-12 00:38:23 +01:00
Robinson c9eca33e89
Updated major version 2022-11-12 00:38:03 +01:00
Robinson 4454d2904e
EndPoint interaction on the network can be overridden for a 100% custom wire protocol 2022-10-02 15:03:28 +02:00
Robinson 700e3ecd7e
Cleaned up formatting/logic of common addresses. Fixed wildcard usage within pollers 2022-08-20 12:15:01 +02:00
Robinson f7c6c88098
More thorough checking for ipv4 remotes when building connections 2022-08-20 11:11:06 +02:00
Robinson 77701e12c4
More specific variable name for ipv4 check 2022-08-20 11:10:19 +02:00
Robinson 5472a07079
Sending chunked data now creates fewer objects while sending. Updated unit test to use proper API 2022-08-19 23:39:54 +02:00
Robinson 48bedc04ca
Cleaned up isWildcard/isLocalhost. removed duplicate code 2022-08-19 23:33:02 +02:00
Robinson 6eb6e677f1
Added comments when sending data + size 2022-08-19 23:31:07 +02:00
Robinson f8e2a16a10
Updated version 2022-08-19 00:50:58 +02:00
Robinson ba423e424f
imports 2022-08-19 00:50:26 +02:00
Robinson 62ec43002b
Added test jar and classes for aeron server/client 2022-08-19 00:50:00 +02:00
Robinson 4af38ef212
Back to suspending 2022-08-18 22:01:42 +02:00
Robinson ae7c043240
Cleaned up logs 2022-08-18 22:00:54 +02:00
Robinson 7e7ccb41da
Fixed log info during memory leak check 2022-08-18 20:44:15 +02:00
Robinson f384638b44
Cleaned up inet address formats 2022-08-18 12:38:44 +02:00
Robinson 9af89ebd0c
Updated comments 2022-08-18 12:38:24 +02:00
Robinson fa95ddf56c
Added extra logs when DEBUG_CONNECTIONS is enabled 2022-08-18 10:18:48 +02:00
Robinson bb499d1c0c
Cleaned up common code and log output 2022-08-18 09:43:38 +02:00
Robinson 2ef56be699
Cleaned up close methods 2022-08-10 14:45:16 +02:00
Robinson a34308ea07
Added driver support for errors and loss statistics 2022-08-10 14:34:54 +02:00
Robinson 4f63dbc25c
Fixed typo in variable name 2022-08-10 14:34:36 +02:00
Robinson ecdd90a657
Updated license 2022-08-10 14:34:14 +02:00
Robinson 5a047e2367
Added support for debugging connections 2022-08-10 14:34:09 +02:00
Robinson 90eab30f48
Updated aeron 2022-08-04 03:49:55 +02:00
Robinson b4b45ba333
commented out/removed unnecessary dependencies 2022-08-04 03:41:42 +02:00
Robinson f429ca1414
Updated version 2022-08-04 03:39:48 +02:00
Robinson dad5cd90b0
Network now uses MDC + unique session id's to initiate/create connections. Prevents issues surrounding handshake conflicts. Now uses exclusive publications, also synchronizes on connection when sending data. Fixed issues when binding to IPv6 Wildcard IPs. RMI events will (attempt) to suppress errors when the connection is closed when the RMI waiter times out. 2022-08-04 03:39:14 +02:00
Robinson 8deee6c0a7
Make sure that the global disconnect handler is also called 2022-08-04 00:34:48 +02:00
189 changed files with 20203 additions and 11100 deletions

3429
LICENSE

File diff suppressed because it is too large Load Diff

373
LICENSE.MPLv2 Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@ -1,89 +1,66 @@
Network
=======
###### [![Dorkbox](https://badge.dorkbox.com/dorkbox.svg "Dorkbox")](https://git.dorkbox.com/dorkbox/Network) [![Github](https://badge.dorkbox.com/github.svg "Github")](https://github.com/dorkbox/Network) [![Gitlab](https://badge.dorkbox.com/gitlab.svg "Gitlab")](https://gitlab.com/dorkbox/Network) [![Bitbucket](https://badge.dorkbox.com/bitbucket.svg "Bitbucket")](https://bitbucket.org/dorkbox/Network)
###### [![Dorkbox](https://badge.dorkbox.com/dorkbox.svg "Dorkbox")](https://git.dorkbox.com/dorkbox/Network) [![Github](https://badge.dorkbox.com/github.svg "Github")](https://github.com/dorkbox/Network) [![Gitlab](https://badge.dorkbox.com/gitlab.svg "Gitlab")](https://gitlab.com/dorkbox/Network)
The Network project is an encrypted, high-performance, event-driven/reactive Network stack with DNS and RMI, using Netty, Kryo, KryoNet RMI, and LZ4 via TCP/UDP.
The Network project is an ~~encrypted~~, high-performance, event-driven/reactive Network stack with DNS and RMI, using Aeron, Kryo, KryoNet RMI, ~~encryption and LZ4 via UDP.~~
These are the main features:
* The connection between endpoints is AES256-GCM / EC curve25519. (WIP, this was updated for use with Aeron, which changes this)
* The connection data is LZ4 compressed and byte-packed for small payload sizes. (WIP, this was updated for use with Aeron, which
changes this)
- The connection supports:
~~* The connection between endpoints is AES256-GCM / EC curve25519. (WIP, this was updated for use with Aeron, which changes this)~~
~~* The connection data is LZ4 compressed and byte-packed for small payload sizes. (WIP, this was updated for use with Aeron, which
changes this)~~
### The connection supports:
- Sending object (via the Kryo serialization framework)
- Sending arbitrarily large objects
- Remote Method Invocation
- Blocking
- Non-Blocking
- Void returns
- Exceptions can be returned
- Kotlin coroutine suspend functions
- Sending data when Idle
- ~~Sending data when Idle~~
- "Pinging" the remote end (for measuring round-trip time)
- Firewall connections by IP+CIDR
- Specify the connection type (nothing, compress, compress+encrypt)
- ~~Specify the connection type (nothing, compress, compress+encrypt)~~
- The available transports is UDP
- The available transports are TCP and UDP
- There are simple wrapper classes for:
- Server
- Client
* MultiCast Broadcast client and server discovery (WIP, this was updated for use with Aeron, which changes this)
- Note: There is a maximum packet size for UDP, 508 bytes *to guarantee it's unfragmented*
- This is for cross-platform use, specifically - linux 32/64, mac 64, and windows 32/64. Java 1.8+
- This is for cross-platform use, specifically - linux 32/64, mac 64, and windows 32/64. Java 1.11+
- This library is designed to be used with kotlin, specifically the use of coroutines.
``` java
public static
class AMessage {
public
AMessage() {
}
val configurationServer = ServerConfiguration()
configurationServer.settingsStore = Storage.Memory() // don't want to persist anything on disk!
configurationServer.port = 2000
configurationServer.enableIPv4 = true
val server: Server<Connection> = Server(configurationServer)
server.onMessage<String> { message ->
logger.error("Received message '$message'")
}
KryoCryptoSerializationManager.DEFAULT.register(AMessage.class);
server.bind()
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
configuration.host = host;
final Server server = new Server(configuration);
addEndPoint(server);
server.bind(false);
server.listeners()
.add(new Listener<AMessage>() {
@Override
public
void received(Connection connection, AMessage object) {
System.err.println("Server received message from client. Bouncing back.");
connection.send()
.TCP(object);
}
});
val configurationClient = ClientConfiguration()
configurationClient.settingsStore = Storage.Memory() // don't want to persist anything on disk!
configurationClient.port = 2000
Client client = new Client(configuration);
client.disableRemoteKeyValidation();
addEndPoint(client);
client.connect(5000);
val client: Client<Connection> = Client(configurationClient)
client.listeners()
.add(new Listener<AMessage>() {
@Override
public
void received(Connection connection, AMessage object) {
ClientSendTest.this.checkPassed.set(true);
System.err.println("Tada! It's been bounced back.");
server.stop();
}
});
client.onConnect {
send("client test message")
}
client.send()
.TCP(new AMessage());
client.connect()
```
&nbsp;
&nbsp;
@ -95,7 +72,7 @@ Maven Info
<dependency>
<groupId>com.dorkbox</groupId>
<artifactId>Network</artifactId>
<version>5.32</version>
<version>6.15</version>
</dependency>
</dependencies>
```
@ -105,11 +82,11 @@ Gradle Info
```
dependencies {
...
implementation("com.dorkbox:Network:5.32")
implementation("com.dorkbox:Network:6.15")
}
```
License
---------
This project is © 2021 dorkbox llc, and is distributed under the terms of the Apache v2.0 License. See file "LICENSE" for further
This project is © 2023 dorkbox llc, and is distributed under the terms of the Apache v2.0 License. See file "LICENSE" for further
references.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,8 +14,6 @@
* limitations under the License.
*/
import java.time.Instant
///////////////////////////////
////// PUBLISH TO SONATYPE / MAVEN CENTRAL
////// TESTING : (to local maven repo) <'publish and release' - 'publishToMavenLocal'>
@ -25,19 +23,22 @@ import java.time.Instant
gradle.startParameter.showStacktrace = ShowStacktrace.ALWAYS // always show the stacktrace!
plugins {
id("com.dorkbox.GradleUtils") version "2.17"
id("com.dorkbox.Licensing") version "2.12"
id("com.dorkbox.VersionUpdate") version "2.5"
id("com.dorkbox.GradlePublish") version "1.12"
id("com.dorkbox.GradleUtils") version "3.18"
id("com.dorkbox.Licensing") version "2.28"
id("com.dorkbox.VersionUpdate") version "2.8"
id("com.dorkbox.GradlePublish") version "1.22"
kotlin("jvm") version "1.6.10"
id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("jvm") version "1.9.0"
}
@Suppress("ConstPropertyName")
object Extras {
// set for the project
const val description = "High-performance, event-driven/reactive network stack for Java 11+"
const val group = "com.dorkbox"
const val version = "5.32"
const val version = "6.15"
// set as project.ext
const val name = "Network"
@ -45,8 +46,6 @@ object Extras {
const val vendor = "Dorkbox LLC"
const val vendorUrl = "https://dorkbox.com"
const val url = "https://git.dorkbox.com/dorkbox/Network"
val buildDate = Instant.now().toString()
}
///////////////////////////////
@ -60,31 +59,38 @@ GradleUtils.compileConfiguration(JavaVersion.VERSION_11) {
// enable the use of inline classes. see https://kotlinlang.org/docs/reference/inline-classes.html
freeCompilerArgs = listOf("-Xinline-classes")
}
//NOTE: we do not support JPMS yet, as there are some libraries missing support for it still
// ratelimiter, "other" package
//val kotlin = project.extensions.getByType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension::class.java).sourceSets.getByName("main").kotlin
//kotlin.apply {
// setSrcDirs(project.files("src"))
// include("**/*.kt") // want to include kotlin files for the source. 'setSrcDirs' resets includes...
//}
// TODO: driver name resolution: https://github.com/real-logic/aeron/wiki/Driver-Name-Resolution
// this keeps us from having to restart the media driver when a connection changes IP addresses
// TODO: virtual threads in java21 for polling?
// if we are sending a SMALL byte array, then we SEND IT DIRECTLY in a more optimized manner (because we can count size info!)
// other side has to be able to parse/know that this was sent directly as bytes. It could be game state data, or voice data, etc.
// another idea is to be able to "send" a stream of bytes (this would also get chunked/etc!). if chunked, these are fixed byte sizes!
// -- the first byte manage: byte/message/stream/etc, no-crypt, crypt, crypt+compress
// - connection.inputStream() --> behaves as an input stream to remote endpoint --> connection.outputStream()
// -- open/close/flush/etc commands also go through
// -- this can be used to stream files/audio/etc VERY easily
// -- have a createInputStream(), which will cause the outputStream() on the remote end to be created.
// --- this remote outputStream is a file, raw??? this is setup by createInputStream() on the remote end
// - state-machine for kryo class registrations?
// ratelimiter, "other" package, send-on-idle
// rest of unit tests
// getConnectionUpgradeType
// ability to send with a function callback (using RMI waiter type stuff for callbacks)
// use conscrypt?!
// java 14 is faster with aeron!
// NOTE: now using aeron instead of netty
// todo: remove BC! use conscrypt instead, or native java? (if possible. we are java 11 now, instead of 1.6)
// also, NOT using bouncastle, but instead the google one
// better SSL library
// implementation("org.conscrypt:conscrypt-openjdk-uber:2.2.1")
// init {
// try {
// Security.insertProviderAt(Conscrypt.newProvider(), 1);
// }
// catch (e: Throwable) {
// e.printStackTrace();
// }
// }
licensing {
@ -134,66 +140,92 @@ tasks.jar.get().apply {
attributes["Specification-Vendor"] = Extras.vendor
attributes["Implementation-Title"] = "${Extras.group}.${Extras.id}"
attributes["Implementation-Version"] = Extras.buildDate
attributes["Implementation-Version"] = GradleUtils.now()
attributes["Implementation-Vendor"] = Extras.vendor
}
}
val shadowJar: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar by tasks
shadowJar.apply {
manifest.inheritFrom(tasks.jar.get().manifest)
manifest.attributes.apply {
put("Main-Class", "dorkboxTest.network.app.AeronClientServerForever")
}
mergeServiceFiles()
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from(sourceSets.test.get().output)
configurations = listOf(project.configurations.testRuntimeClasspath.get())
archiveBaseName.set(project.name + "-all")
}
dependencies {
api("org.jetbrains.kotlinx:atomicfu:0.17.3")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
api("org.jetbrains.kotlinx:atomicfu:0.23.0")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
// https://github.com/dorkbox
api("com.dorkbox:ByteUtilities:1.5")
api("com.dorkbox:Collections:1.1")
api("com.dorkbox:MinLog:2.4")
api("com.dorkbox:NetworkDNS:2.7.1")
api("com.dorkbox:NetworkUtils:2.18")
api("com.dorkbox:ObjectPool:4.0")
api("com.dorkbox:OS:1.0")
api("com.dorkbox:Serializers:2.7")
api("com.dorkbox:Storage:1.1")
api("com.dorkbox:ByteUtilities:2.1")
api("com.dorkbox:ClassUtils:1.3")
api("com.dorkbox:Collections:2.7")
api("com.dorkbox:HexUtilities:1.1")
api("com.dorkbox:JNA:1.4")
api("com.dorkbox:MinLog:2.7")
api("com.dorkbox:NetworkDNS:2.16")
api("com.dorkbox:NetworkUtils:2.23")
api("com.dorkbox:OS:1.11")
api("com.dorkbox:Serializers:2.9")
api("com.dorkbox:Storage:1.11")
api("com.dorkbox:Updates:1.1")
api("com.dorkbox:Utilities:1.29")
api("com.dorkbox:Utilities:1.48")
// how we bypass using reflection/jpms to access fields for java17+
api("org.javassist:javassist:3.29.2-GA")
val jnaVersion = "5.13.0"
api("net.java.dev.jna:jna-jpms:${jnaVersion}")
api("net.java.dev.jna:jna-platform-jpms:${jnaVersion}")
// we include ALL of aeron, in case we need to debug aeron behavior
// https://github.com/real-logic/aeron
val aeronVer = "1.38.1"
api("io.aeron:aeron-all:$aeronVer")
// api("io.aeron:aeron-client:$aeronVer")
// api("io.aeron:aeron-driver:$aeronVer")
val aeronVer = "1.42.1"
api("io.aeron:aeron-driver:$aeronVer")
// ALL of aeron, in case we need to debug aeron behavior
// api("io.aeron:aeron-all:$aeronVer")
// api("org.agrona:agrona:1.18.2") // sources for this aren't included in aeron-all!
// https://github.com/EsotericSoftware/kryo
api("com.esotericsoftware:kryo:5.3.0") {
api("com.esotericsoftware:kryo:5.5.0") {
exclude("com.esotericsoftware", "minlog") // we use our own minlog, that logs to SLF4j instead
}
// https://github.com/jpountz/lz4-java
// implementation("net.jpountz.lz4:lz4:1.3.0")
// this is NOT the same thing as LMAX disruptor.
// This is just a slightly faster queue than LMAX. (LMAX is a fast queue + other things w/ a difficult DSL)
// https://github.com/conversant/disruptor_benchmark
// https://www.youtube.com/watch?v=jVMOgQgYzWU
//api("com.conversantmedia:disruptor:1.2.19")
// https://github.com/lz4/lz4-java
api("org.lz4:lz4-java:1.8.0")
// https://github.com/jhalterman/typetools
api("net.jodah:typetools:0.6.3")
// Expiring Map (A high performance thread-safe map that expires entries)
// https://github.com/jhalterman/expiringmap
api("net.jodah:expiringmap:0.5.10")
api("net.jodah:expiringmap:0.5.11")
// https://github.com/MicroUtils/kotlin-logging
api("io.github.microutils:kotlin-logging:2.1.23")
api("org.slf4j:slf4j-api:1.8.0-beta4")
// api("io.github.microutils:kotlin-logging:3.0.5")
implementation("org.slf4j:slf4j-api:2.0.9")
testImplementation("junit:junit:4.13.2")
testImplementation("ch.qos.logback:logback-classic:1.3.0-alpha4")
testImplementation("ch.qos.logback:logback-classic:1.4.5")
testImplementation("io.aeron:aeron-all:$aeronVer")
testImplementation("com.dorkbox:Config:2.1")
}
publishToSonatype {

Binary file not shown.

View File

@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

31
gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +80,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,22 +131,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,6 +198,10 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
@ -205,6 +214,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

15
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@ -1,293 +0,0 @@
package dorkbox.network.other
import java.math.BigInteger
import java.security.GeneralSecurityException
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.SecureRandom
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.ECPublicKey
import java.security.spec.ECField
import java.security.spec.ECFieldFp
import java.security.spec.ECParameterSpec
import java.security.spec.ECPoint
import java.security.spec.ECPublicKeySpec
import java.security.spec.EllipticCurve
import java.security.spec.NamedParameterSpec
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
/**
*
*/
private object CryptoEccNative {
// see: https://openjdk.java.net/jeps/324
const val curve25519 = "curve25519"
const val default_curve = curve25519
const val macSize = 512
// on NIST vs 25519 vs Brainpool, see:
// - http://ogryb.blogspot.de/2014/11/why-i-dont-trust-nist-p-256.html
// - http://credelius.com/credelius/?p=97
// - http://safecurves.cr.yp.to/
// we should be using 25519, because NIST and brainpool are "unsafe". Brainpool is "more random" than 25519, but is still not considered safe.
// more info about ECC from:
// http://www.johannes-bauer.com/compsci/ecc/?menuid=4
// http://stackoverflow.com/questions/7419183/problems-implementing-ecdh-on-android-using-bouncycastle
// http://tools.ietf.org/html/draft-jivsov-openpgp-ecc-06#page-4
// http://www.nsa.gov/ia/programs/suiteb_cryptography/
// https://github.com/nelenkov/ecdh-kx/blob/master/src/org/nick/ecdhkx/Crypto.java
// http://nelenkov.blogspot.com/2011/12/using-ecdh-on-android.html
// http://www.secg.org/collateral/sec1_final.pdf
// More info about 25519 key types (ed25519 and X25519)
// https://blog.filippo.io/using-ed25519-keys-for-encryption/
fun createKeyPair(secureRandom: SecureRandom): KeyPair {
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance("XDH")
kpg.initialize(NamedParameterSpec.X25519, secureRandom)
return kpg.generateKeyPair()
// println("--- Public Key ---")
// val publicKey = kp.public
//
// System.out.println(publicKey.algorithm) // XDH
// System.out.println(publicKey.format) // X.509
//
// // save this public key
// val pubKey = publicKey.encoded
//
// println("---")
//
// println("--- Private Key ---")
// val privateKey = kp.private
//
// System.out.println(privateKey.algorithm); // XDH
// System.out.println(privateKey.format); // PKCS#8
//
// // save this private key
// val priKey = privateKey.encoded
// val kf: KeyFactory = KeyFactory.getInstance("XDH");
// //BigInteger u = ...
// val pubSpec: XECPublicKeySpec = XECPublicKeySpec(paramSpec, u);
// val pubKey: PublicKey = kf.generatePublic(pubSpec);
// //
//
// val ka: KeyAgreement = KeyAgreement.getInstance("XDH");
// ka.init(kp.private);
//ka.doPhase(pubKey, true);
//byte[] secret = ka.generateSecret();
}
private val FieldP_2: BigInteger = BigInteger.TWO // constant for scalar operations
private val FieldP_3: BigInteger = BigInteger.valueOf(3) // constant for scalar operations
private const val byteVal1 = 1.toByte()
@Throws(GeneralSecurityException::class)
fun getPublicKey(pk: ECPrivateKey): ECPublicKey? {
val params: ECParameterSpec = pk.params
val w: ECPoint = scalmultNew(params, params.generator, pk.s)
//final ECPoint w = scalmult(params.getCurve(), pk.getParams().getGenerator(), pk.getS());
val kg: KeyFactory = KeyFactory.getInstance("EC")
return kg.generatePublic(ECPublicKeySpec(w, params)) as ECPublicKey
}
private fun scalmultNew(params: ECParameterSpec, g: ECPoint, kin: BigInteger): ECPoint {
val curve = params.curve
val field = curve.field
if (field !is ECFieldFp) throw java.lang.UnsupportedOperationException(field::class.java.canonicalName)
val p = field.p
val a = curve.a
var R = ECPoint.POINT_INFINITY
// value only valid for curve secp256k1, code taken from https://www.secg.org/sec2-v2.pdf,
// see "Finally the order n of G and the cofactor are: n = "FF.."
val SECP256K1_Q = params.order
//BigInteger SECP256K1_Q = new BigInteger("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",16);
var k = kin.mod(SECP256K1_Q) // uses this !
// BigInteger k = kin.mod(p); // do not use this ! wrong as per comment from President James Moveon Polk
val length = k.bitLength()
val binarray = ByteArray(length)
for (i in 0..length - 1) {
binarray[i] = k.mod(FieldP_2).byteValueExact()
k = k.shiftRight(1)
}
for (i in length - 1 downTo 0) {
R = doublePoint(p, a, R)
if (binarray[i] == byteVal1) R = addPoint(p, a, R, g)
}
return R
}
fun scalmultOrg(curve: EllipticCurve, g: ECPoint, kin: BigInteger): ECPoint {
val field: ECField = curve.getField()
if (field !is ECFieldFp) throw UnsupportedOperationException(field::class.java.canonicalName)
val p: BigInteger = (field as ECFieldFp).getP()
val a: BigInteger = curve.getA()
var R = ECPoint.POINT_INFINITY
// value only valid for curve secp256k1, code taken from https://www.secg.org/sec2-v2.pdf,
// see "Finally the order n of G and the cofactor are: n = "FF.."
val SECP256K1_Q = BigInteger("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
var k = kin.mod(SECP256K1_Q) // uses this !
// wrong as per comment from President James Moveon Polk
// BigInteger k = kin.mod(p); // do not use this !
println(" SECP256K1_Q: $SECP256K1_Q")
println(" p: $p")
System.out.println("curve: " + curve.toString())
val length = k.bitLength()
val binarray = ByteArray(length)
for (i in 0..length - 1) {
binarray[i] = k.mod(FieldP_2).byteValueExact()
k = k.shiftRight(1)
}
for (i in length - 1 downTo 0) {
R = doublePoint(p, a, R)
if (binarray[i] == byteVal1) R = addPoint(p, a, R, g)
}
return R
}
// scalar operations for native java
// https://stackoverflow.com/a/42797410/8166854
// written by author: SkateScout
private fun doublePoint(p: BigInteger, a: BigInteger, R: ECPoint): ECPoint? {
if (R == ECPoint.POINT_INFINITY) return R
var slope = R.affineX.pow(2).multiply(FieldP_3)
slope = slope.add(a)
slope = slope.multiply(R.affineY.multiply(FieldP_2).modInverse(p))
val Xout = slope.pow(2).subtract(R.affineX.multiply(FieldP_2)).mod(p)
val Yout = R.affineY.negate().add(slope.multiply(R.affineX.subtract(Xout))).mod(p)
return ECPoint(Xout, Yout)
}
private fun addPoint(p: BigInteger, a: BigInteger, r: ECPoint, g: ECPoint): ECPoint? {
if (r == ECPoint.POINT_INFINITY) return g
if (g == ECPoint.POINT_INFINITY) return r
if (r == g || r == g) return doublePoint(p, a, r)
val gX = g.affineX
val sY = g.affineY
val rX = r.affineX
val rY = r.affineY
val slope = rY.subtract(sY).multiply(rX.subtract(gX).modInverse(p)).mod(p)
val Xout = slope.modPow(FieldP_2, p).subtract(rX).subtract(gX).mod(p)
var Yout = sY.negate().mod(p)
Yout = Yout.add(slope.multiply(gX.subtract(Xout))).mod(p)
return ECPoint(Xout, Yout)
}
private fun byteArrayToHexString(a: ByteArray): String {
val sb = StringBuilder(a.size * 2)
for (b in a) sb.append(String.format("%02X", b))
return sb.toString()
}
fun hexStringToByteArray(s: String): ByteArray {
val len = s.length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
data[i / 2] = ((Character.digit(s[i], 16) shl 4)
+ Character.digit(s[i + 1], 16)).toByte()
i += 2
}
return data
}
@Throws(GeneralSecurityException::class)
@JvmStatic
fun main(args: Array<String>) {
val cryptoText = "i23j4jh234kjh234kjh23lkjnfa9s8egfuypuh325"
// NOTE: THIS IS NOT 25519!!
println("Generate ECPublicKey from PrivateKey (String) for curve secp256k1 (final)")
println("Check keys with https://gobittest.appspot.com/Address")
// https://gobittest.appspot.com/Address
val privateKey = "D12D2FACA9AD92828D89683778CB8DFCCDBD6C9E92F6AB7D6065E8AACC1FF6D6"
val publicKeyExpected = "04661BA57FED0D115222E30FE7E9509325EE30E7E284D3641E6FB5E67368C2DB185ADA8EFC5DC43AF6BF474A41ED6237573DC4ED693D49102C42FFC88510500799"
println("\nprivatekey given : $privateKey")
println("publicKeyExpected: $publicKeyExpected")
// // routine with bouncy castle
// println("\nGenerate PublicKey from PrivateKey with BouncyCastle")
// val spec: ECNamedCurveParameterSpec = ECNamedCurveTable.getParameterSpec("secp256k1") // this ec curve is used for bitcoin operations
// val pointQ: org.bouncycastle.math.ec.ECPoint = spec.getG().multiply(BigInteger(1, ch.qos.logback.core.encoder.ByteArrayUtil.hexStringToByteArray(privateKey)))
// val publickKeyByte = pointQ.getEncoded(false)
// val publicKeyBc: String = byteArrayToHexString(publickKeyByte)
// println("publicKeyExpected: $publicKeyExpected")
// println("publicKey BC : $publicKeyBc")
// println("publicKeys match : " + publicKeyBc.contentEquals(publicKeyExpected))
// regeneration of ECPublicKey with java native starts here
println("\nGenerate PublicKey from PrivateKey with Java native routines")
// the preset "303E.." only works for elliptic curve secp256k1
// see answer by user dave_thompson_085
// https://stackoverflow.com/questions/48832170/generate-ec-public-key-from-byte-array-private-key-in-native-java-7
val privateKeyFull = "303E020100301006072A8648CE3D020106052B8104000A042730250201010420" + privateKey
val privateKeyFullByte: ByteArray = hexStringToByteArray(privateKeyFull)
println("privateKey full : $privateKeyFull")
val keyFactory = KeyFactory.getInstance("EC")
val privateKeyNative: PrivateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(privateKeyFullByte))
val ecPrivateKeyNative = privateKeyNative as ECPrivateKey
val ecPublicKeyNative = getPublicKey(ecPrivateKeyNative)
val ecPublicKeyNativeByte = ecPublicKeyNative!!.encoded
val testPubKey = keyFactory.generatePublic(X509EncodedKeySpec(ecPublicKeyNativeByte)) as ECPublicKey
val equal = ecPublicKeyNativeByte.contentEquals(testPubKey.encoded)
val publicKeyNativeFull: String = byteArrayToHexString(ecPublicKeyNativeByte)
val publicKeyNativeHeader = publicKeyNativeFull.substring(0, 46)
val publicKeyNativeKey = publicKeyNativeFull.substring(46, 176)
println("ecPublicKeyFull : $publicKeyNativeFull")
println("ecPublicKeyHeader: $publicKeyNativeHeader")
println("ecPublicKeyKey : $publicKeyNativeKey")
println("publicKeyExpected: $publicKeyExpected")
println("publicKeys match : " + publicKeyNativeKey.contentEquals(publicKeyExpected))
// encrypt
val encryptCipher: Cipher = Cipher.getInstance("RSA")
encryptCipher.init(Cipher.ENCRYPT_MODE, ecPublicKeyNative)
val cipherText: ByteArray = encryptCipher.doFinal(cryptoText.toByteArray())
// decrypt
val decryptCipher = Cipher.getInstance("RSA");
decryptCipher.init(Cipher.DECRYPT_MODE, ecPrivateKeyNative);
val outputBytes = decryptCipher.doFinal(cipherText)
println("Crypto round passed: ${String(outputBytes) == cryptoText}")
}
}

View File

@ -1,159 +0,0 @@
/* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import static org.junit.Assert.fail;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
import dorkbox.network.serialization.Serialization;
import dorkbox.util.exceptions.SecurityException;
import dorkbox.util.serialization.SerializationManager;
public
class LargeResizeBufferTest extends BaseTest {
private static final int OBJ_SIZE = 1024 * 100;
private volatile int finalCheckAmount = 0;
private volatile int serverCheck = -1;
private volatile int clientCheck = -1;
@Test
public
void manyLargeMessages() throws SecurityException, IOException {
final int messageCount = 1024;
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
configuration.udpPort = udpPort;
configuration.host = host;
register(configuration.serialization);
Server server = new Server(configuration);
addEndPoint(server);
server.bind(false);
server.listeners()
.add(new Listener.OnMessageReceived<Connection, LargeMessage>() {
AtomicInteger received = new AtomicInteger();
AtomicInteger receivedBytes = new AtomicInteger();
@Override
public
void received(Connection connection, LargeMessage object) {
// System.err.println("Server ack message: " + received.get());
connection.send()
.TCP(object);
this.receivedBytes.addAndGet(object.bytes.length);
if (this.received.incrementAndGet() == messageCount) {
System.out.println("Server received all " + messageCount + " messages!");
System.out.println("Server received and sent " + this.receivedBytes.get() + " bytes.");
LargeResizeBufferTest.this.serverCheck = LargeResizeBufferTest.this.finalCheckAmount - this.receivedBytes.get();
System.out.println("Server missed " + LargeResizeBufferTest.this.serverCheck + " bytes.");
stopEndPoints();
}
}
});
Client client = new Client(configuration);
addEndPoint(client);
client.listeners()
.add(new Listener.OnMessageReceived<Connection, LargeMessage>() {
AtomicInteger received = new AtomicInteger();
AtomicInteger receivedBytes = new AtomicInteger();
@Override
public
void received(Connection connection, LargeMessage object) {
this.receivedBytes.addAndGet(object.bytes.length);
int count = this.received.getAndIncrement();
// System.out.println("Client received message: " + count);
if (count == messageCount) {
System.out.println("Client received all " + messageCount + " messages!");
System.out.println("Client received and sent " + this.receivedBytes.get() + " bytes.");
LargeResizeBufferTest.this.clientCheck = LargeResizeBufferTest.this.finalCheckAmount - this.receivedBytes.get();
System.out.println("Client missed " + LargeResizeBufferTest.this.clientCheck + " bytes.");
}
}
});
client.connect(5000);
SecureRandom random = new SecureRandom();
System.err.println(" Client sending " + messageCount + " messages");
for (int i = 0; i < messageCount; i++) {
this.finalCheckAmount += OBJ_SIZE; // keep increasing size
byte[] b = new byte[OBJ_SIZE];
random.nextBytes(b);
// set some of the bytes to be all `244`, just so some compression can occur (to test that as well)
for (int j = 0; j < 400; j++) {
b[j] = (byte) 244;
}
// System.err.println("Sending " + b.length + " bytes");
client.send()
.TCP(new LargeMessage(b));
}
System.err.println("Client has queued " + messageCount + " messages.");
waitForThreads();
if (this.clientCheck > 0) {
fail("Client missed " + this.clientCheck + " bytes.");
}
if (this.serverCheck > 0) {
fail("Server missed " + this.serverCheck + " bytes.");
}
}
private
void register(SerializationManager manager) {
manager.register(byte[].class);
manager.register(LargeMessage.class);
}
public static
class LargeMessage {
public byte[] bytes;
public
LargeMessage() {
}
public
LargeMessage(byte[] bytes) {
this.bytes = bytes;
}
}
}

View File

@ -1,197 +0,0 @@
package dorkbox.network.other
import kotlin.math.ceil
/**
*
*/
object Misc {
private fun annotations() {
// internal val classesWithRmiFields = IdentityMap<Class<*>, Array<Field>>()
// // get all classes that have fields with @Rmi field annotation.
// // THESE classes must be customized with our special RmiFieldSerializer serializer so that the @Rmi field is properly handled
//
// // SPECIFICALLY, these fields must also be an IFACE for the field type!
//
// // NOTE: The @Rmi field type will already have to be a registered type with kryo!
// // we can use this information on WHERE to scan for classes.
// val filesToScan = mutableSetOf<File>()
//
// classesToRegister.forEach { registration ->
// val clazz = registration.clazz
//
// // can't do anything if codeSource is null!
// val codeSource = clazz.protectionDomain.codeSource ?: return@forEach
// // file:/Users/home/java/libs/xyz-123.jar
// // file:/projects/classes
// val jarOrClassPath = codeSource.location.toString()
//
// if (jarOrClassPath.endsWith(".jar")) {
// val fileName: String = URLDecoder.decode(jarOrClassPath.substring("file:".length), Charset.defaultCharset())
// filesToScan.add(File(fileName).absoluteFile)
// } else {
// val classPath: String = URLDecoder.decode(jarOrClassPath.substring("file:".length), Charset.defaultCharset())
// filesToScan.add(File(classPath).absoluteFile)
// }
// }
//
// val toTypedArray = filesToScan.toTypedArray()
// if (logger.isTraceEnabled) {
// toTypedArray.forEach {
// logger.trace { "Adding location to annotation scanner: $it"}
// }
// }
//
//
//
// // now scan these jars/directories
// val fieldsWithRmiAnnotation = AnnotationDetector.scanFiles(*toTypedArray)
// .forAnnotations(Rmi::class.java)
// .on(ElementType.FIELD)
// .collect { cursor -> Pair(cursor.type, cursor.field!!) }
//
// // have to make sure that the field type is specified as an interface (and not an implementation)
// fieldsWithRmiAnnotation.forEach { pair ->
// require(pair.second.type.isInterface) { "@Rmi annotated fields must be an interface!" }
// }
//
// if (fieldsWithRmiAnnotation.isNotEmpty()) {
// logger.info { "Verifying scanned classes containing @Rmi field annotations" }
// }
//
// // have to put this in a map, so we can quickly lookup + get the fields later on.
// // NOTE: a single class can have MULTIPLE fields with @Rmi annotations!
// val rmiAnnotationMap = IdentityMap<Class<*>, MutableList<Field>>()
// fieldsWithRmiAnnotation.forEach {
// var fields = rmiAnnotationMap[it.first]
// if (fields == null) {
// fields = mutableListOf()
// }
//
// fields.add(it.second)
// rmiAnnotationMap.put(it.first, fields)
// }
//
// // now make it an array for fast lookup for the [parent class] -> [annotated fields]
// rmiAnnotationMap.forEach {
// classesWithRmiFields.put(it.key, it.value.toTypedArray())
// }
//
// // this will set up the class registration information
// initKryo()
//
// // now everything is REGISTERED, possibly with custom serializers, we have to go back and change them to use our RmiFieldSerializer
// fieldsWithRmiAnnotation.forEach FIELD_SCAN@{ pair ->
// // the parent class must be an IMPL. The reason is that THIS FIELD will be sent as a RMI object, and this can only
// // happen on objects that exist
//
// // NOTE: it IS necessary for the rmi-client to be aware of the @Rmi annotation (because it also has to have the correct serialization)
//
// // also, it is possible for the class that has the @Rmi field to be a NORMAL object (and not an RMI object)
// // this means we found the registration for the @Rmi field annotation
//
// val parentRmiRegistration = classesToRegister.firstOrNull { it is ClassRegistrationForRmi && it.implClass == pair.first}
//
//
// // if we have a parent-class registration, this means we are the rmi-server
// //
// // AND BECAUSE OF THIS
// //
// // we must also have the field type registered as RMI
// if (parentRmiRegistration != null) {
// // rmi-server
//
// // is the field type registered also?
// val fieldRmiRegistration = classesToRegister.firstOrNull { it.clazz == pair.second.type}
// require(fieldRmiRegistration is ClassRegistrationForRmi) { "${pair.second.type} is not registered for RMI! Unable to continue"}
//
// logger.trace { "Found @Rmi field annotation '${pair.second.type}' in class '${pair.first}'" }
// } else {
// // rmi-client
//
// // NOTE: rmi-server MUST have the field IMPL registered (ie: via RegisterRmi)
// // rmi-client will have the serialization updated from the rmi-server during connection handshake
// }
// }
}
/**
* Split array into chunks, max of 256 chunks.
* byte[0] = chunk ID
* byte[1] = total chunks (0-255) (where 0->1, 2->3, 127->127 because this is indexed by a byte)
*/
private fun divideArray(source: ByteArray, chunksize: Int): Array<ByteArray>? {
val fragments = ceil(source.size / chunksize.toDouble()).toInt()
if (fragments > 127) {
// cannot allow more than 127
return null
}
// pre-allocate the memory
val splitArray = Array(fragments) { ByteArray(chunksize + 2) }
var start = 0
for (i in splitArray.indices) {
var length = if (start + chunksize > source.size) {
source.size - start
} else {
chunksize
}
splitArray[i] = ByteArray(length + 2)
splitArray[i][0] = i.toByte() // index
splitArray[i][1] = fragments.toByte() // total number of fragments
System.arraycopy(source, start, splitArray[i], 2, length)
start += chunksize
}
return splitArray
}
}
// fun initClassRegistration(channel: Channel, registration: Registration): Boolean {
// val details = serialization.getKryoRegistrationDetails()
// val length = details.size
// if (length > Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE) {
// // it is too large to send in a single packet
//
// // child arrays have index 0 also as their 'index' and 1 is the total number of fragments
// val fragments = divideArray(details, Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE)
// if (fragments == null) {
// logger.error("Too many classes have been registered for Serialization. Please report this issue")
// return false
// }
// val allButLast = fragments.size - 1
// for (i in 0 until allButLast) {
// val fragment = fragments[i]
// val fragmentedRegistration = Registration.hello(registration.oneTimePad, config.settingsStore.getPublicKey())
// fragmentedRegistration.payload = fragment
//
// // tell the server we are fragmented
// fragmentedRegistration.upgradeType = UpgradeType.FRAGMENTED
//
// // tell the server we are upgraded (it will bounce back telling us to connect)
// fragmentedRegistration.upgraded = true
// channel.writeAndFlush(fragmentedRegistration)
// }
//
// // now tell the server we are done with the fragments
// val fragmentedRegistration = Registration.hello(registration.oneTimePad, config.settingsStore.getPublicKey())
// fragmentedRegistration.payload = fragments[allButLast]
//
// // tell the server we are fragmented
// fragmentedRegistration.upgradeType = UpgradeType.FRAGMENTED
//
// // tell the server we are upgraded (it will bounce back telling us to connect)
// fragmentedRegistration.upgraded = true
// channel.writeAndFlush(fragmentedRegistration)
// } else {
// registration.payload = details
//
// // tell the server we are upgraded (it will bounce back telling us to connect)
// registration.upgraded = true
// channel.writeAndFlush(registration)
// }
// return true
// }

View File

@ -1,242 +0,0 @@
/* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
import dorkbox.network.connection.Listeners;
import dorkbox.network.serialization.Serialization;
import dorkbox.util.exceptions.SecurityException;
public
class MultipleThreadTest extends BaseTest {
private final Object lock = new Object();
private volatile boolean stillRunning = false;
private final Object finalRunLock = new Object();
private volatile boolean finalStillRunning = false;
private final int messageCount = 150;
private final int threadCount = 15;
private final int clientCount = 13;
private final List<Client> clients = new ArrayList<Client>(this.clientCount);
int perClientReceiveTotal = (this.messageCount * this.threadCount);
int serverReceiveTotal = perClientReceiveTotal * this.clientCount;
AtomicInteger sent = new AtomicInteger(0);
AtomicInteger totalClientReceived = new AtomicInteger(0);
AtomicInteger receivedServer = new AtomicInteger(1);
ConcurrentHashMap<Integer, DataClass> sentStringsToClientDebug = new ConcurrentHashMap<Integer, DataClass>();
@Test
public
void multipleThreads() throws SecurityException, IOException {
// our clients should receive messageCount * threadCount * clientCount TOTAL messages
final int totalClientReceivedCountExpected = this.clientCount * this.messageCount * this.threadCount;
final int totalServerReceivedCountExpected = this.clientCount * this.messageCount;
System.err.println("CLIENT RECEIVES: " + totalClientReceivedCountExpected);
System.err.println("SERVER RECEIVES: " + totalServerReceivedCountExpected);
Configuration configuration = new Configuration();
configuration.tcpPort = tcpPort;
configuration.host = host;
configuration.serialization.register(String[].class);
configuration.serialization.register(DataClass.class);
final Server server = new Server(configuration);
server.disableRemoteKeyValidation();
addEndPoint(server);
server.bind(false);
final Listeners listeners = server.listeners();
listeners.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(final Connection connection) {
System.err.println("Client connected to server.");
// kickoff however many threads we need, and send data to the client.
for (int i = 1; i <= MultipleThreadTest.this.threadCount; i++) {
final int index = i;
new Thread() {
@Override
public
void run() {
for (int i = 1; i <= MultipleThreadTest.this.messageCount; i++) {
int incrementAndGet = MultipleThreadTest.this.sent.getAndIncrement();
DataClass dataClass = new DataClass("Server -> client. Thread #" + index + " message# " + incrementAndGet,
incrementAndGet);
//System.err.println(dataClass.data);
MultipleThreadTest.this.sentStringsToClientDebug.put(incrementAndGet, dataClass);
connection.send()
.TCP(dataClass)
.flush();
}
}
}.start();
}
}
});
listeners.add(new Listener.OnMessageReceived<Connection, DataClass>() {
@Override
public
void received(Connection connection, DataClass object) {
int incrementAndGet = MultipleThreadTest.this.receivedServer.getAndIncrement();
//System.err.println("server #" + incrementAndGet);
if (incrementAndGet % MultipleThreadTest.this.messageCount == 0) {
System.err.println("Server receive DONE for client " + incrementAndGet);
stillRunning = false;
synchronized (MultipleThreadTest.this.lock) {
MultipleThreadTest.this.lock.notifyAll();
}
}
if (incrementAndGet == totalServerReceivedCountExpected) {
System.err.println("Server DONE: " + incrementAndGet);
finalStillRunning = false;
synchronized (MultipleThreadTest.this.finalRunLock) {
MultipleThreadTest.this.finalRunLock.notifyAll();
}
}
}
});
// ----
finalStillRunning = true;
for (int i = 1; i <= this.clientCount; i++) {
final int index = i;
Client client = new Client(configuration);
this.clients.add(client);
addEndPoint(client);
client.listeners()
.add(new Listener.OnMessageReceived<Connection, DataClass>() {
final int clientIndex = index;
final AtomicInteger received = new AtomicInteger(1);
@Override
public
void received(Connection connection, DataClass object) {
totalClientReceived.getAndIncrement();
int clientLocalCounter = this.received.getAndIncrement();
MultipleThreadTest.this.sentStringsToClientDebug.remove(object.index);
//System.err.println(object.data);
// we finished!!
if (clientLocalCounter == perClientReceiveTotal) {
//System.err.println("Client #" + clientIndex + " received " + clientLocalCounter + " Sending back " +
// MultipleThreadTest.this.messageCount + " messages.");
// now spam back messages!
for (int i = 0; i < MultipleThreadTest.this.messageCount; i++) {
connection.send()
.TCP(new DataClass("Client #" + clientIndex + " -> Server message " + i, index));
}
}
}
});
stillRunning = true;
client.connect(5000);
while (stillRunning) {
synchronized (this.lock) {
try {
this.lock.wait(5 * 1000); // 5 secs
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
while (finalStillRunning) {
synchronized (this.finalRunLock) {
try {
this.finalRunLock.wait(5 * 1000); // 5 secs
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// CLIENT will wait until it's done connecting, but SERVER is async.
// the ONLY way to safely work in the server is with LISTENERS. Everything else can FAIL, because of it's async nature.
if (!this.sentStringsToClientDebug.isEmpty()) {
System.err.println("MISSED DATA: " + this.sentStringsToClientDebug.size());
for (Map.Entry<Integer, DataClass> i : this.sentStringsToClientDebug.entrySet()) {
System.err.println(i.getKey() + " : " + i.getValue().data);
}
}
stopEndPoints();
assertEquals(totalClientReceivedCountExpected, totalClientReceived.get());
// offset by 1 since we start at 1
assertEquals(totalServerReceivedCountExpected, receivedServer.get()-1);
}
public static
class DataClass {
public String data;
public Integer index;
public
DataClass() {
}
public
DataClass(String data, Integer index) {
this.data = data;
this.index = index;
}
}
}

View File

@ -1,326 +0,0 @@
/* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
import dorkbox.network.connection.Listeners;
import dorkbox.network.serialization.Serialization;
import dorkbox.util.exceptions.SecurityException;
import dorkbox.util.serialization.SerializationManager;
public
class PingPongLocalTest extends BaseTest {
int tries = 10000;
private volatile String fail;
@Test
public void pingPongLocal() throws SecurityException, IOException {
this.fail = "Data not received.";
final Data dataLOCAL = new Data();
populateData(dataLOCAL);
Configuration configuration = Configuration.localOnly();
register(configuration.serialization);
Server server = new Server(configuration);
addEndPoint(server);
server.bind(false);
final Listeners listeners = server.listeners();
listeners.add(new Listener.OnError<Connection>() {
@Override
public
void error(Connection connection, Throwable throwable) {
PingPongLocalTest.this.fail = "Error during processing. " + throwable;
}
});
listeners.add(new Listener.OnMessageReceived<Connection, Data>() {
@Override
public
void received(Connection connection, Data data) {
connection.id();
if (!data.equals(dataLOCAL)) {
PingPongLocalTest.this.fail = "data is not equal on server.";
throw new RuntimeException("Fail! " + PingPongLocalTest.this.fail);
}
connection.send()
.TCP(data);
}
});
// ----
Client client = new Client(configuration);
addEndPoint(client);
final Listeners listeners1 = client.listeners();
listeners1.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(Connection connection) {
PingPongLocalTest.this.fail = null;
connection.send()
.TCP(dataLOCAL);
// connection.sendUDP(dataUDP); // TCP and UDP are the same for a local channel.
}
});
listeners1.add(new Listener.OnError<Connection>() {
@Override
public
void error(Connection connection, Throwable throwable) {
PingPongLocalTest.this.fail = "Error during processing. " + throwable;
System.err.println(PingPongLocalTest.this.fail);
}
});
listeners1.add(new Listener.OnMessageReceived<Connection, Data>() {
AtomicInteger check = new AtomicInteger(0);
@Override
public
void received(Connection connection, Data data) {
if (!data.equals(dataLOCAL)) {
PingPongLocalTest.this.fail = "data is not equal on client.";
throw new RuntimeException("Fail! " + PingPongLocalTest.this.fail);
}
if (this.check.getAndIncrement() <= PingPongLocalTest.this.tries) {
connection.send()
.TCP(data);
}
else {
System.err.println("Ran LOCAL " + PingPongLocalTest.this.tries + " times");
stopEndPoints();
}
}
});
client.connect(5000);
waitForThreads();
if (this.fail != null) {
fail(this.fail);
}
}
private void populateData(Data data) {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < 3000; i++) {
buffer.append('a');
}
data.string = buffer.toString();
data.strings = new String[] {"abcdefghijklmnopqrstuvwxyz0123456789","",null,"!@#$","<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"};
data.ints = new int[] {-1234567,1234567,-1,0,1,Integer.MAX_VALUE,Integer.MIN_VALUE};
data.shorts = new short[] {-12345,12345,-1,0,1,Short.MAX_VALUE,Short.MIN_VALUE};
data.floats = new float[] {0,-0,1,-1,123456,-123456,0.1f,0.2f,-0.3f,(float) Math.PI,Float.MAX_VALUE,
Float.MIN_VALUE};
data.doubles = new double[] {0,-0,1,-1,123456,-123456,0.1d,0.2d,-0.3d,Math.PI,Double.MAX_VALUE,Double.MIN_VALUE};
data.longs = new long[] {0,-0,1,-1,123456,-123456,99999999999l,-99999999999l,Long.MAX_VALUE,Long.MIN_VALUE};
data.bytes = new byte[] {-123,123,-1,0,1,Byte.MAX_VALUE,Byte.MIN_VALUE};
data.chars = new char[] {32345,12345,0,1,63,Character.MAX_VALUE,Character.MIN_VALUE};
data.booleans = new boolean[] {true,false};
data.Ints = new Integer[] {-1234567,1234567,-1,0,1,Integer.MAX_VALUE,Integer.MIN_VALUE};
data.Shorts = new Short[] {-12345,12345,-1,0,1,Short.MAX_VALUE,Short.MIN_VALUE};
data.Floats = new Float[] {0f,-0f,1f,-1f,123456f,-123456f,0.1f,0.2f,-0.3f,(float) Math.PI,Float.MAX_VALUE,
Float.MIN_VALUE};
data.Doubles = new Double[] {0d,-0d,1d,-1d,123456d,-123456d,0.1d,0.2d,-0.3d,Math.PI,Double.MAX_VALUE,
Double.MIN_VALUE};
data.Longs = new Long[] {0l,-0l,1l,-1l,123456l,-123456l,99999999999l,-99999999999l,Long.MAX_VALUE,
Long.MIN_VALUE};
data.Bytes = new Byte[] {-123,123,-1,0,1,Byte.MAX_VALUE,Byte.MIN_VALUE};
data.Chars = new Character[] {32345,12345,0,1,63,Character.MAX_VALUE,Character.MIN_VALUE};
data.Booleans = new Boolean[] {true,false};
}
private void register(SerializationManager manager) {
manager.register(int[].class);
manager.register(short[].class);
manager.register(float[].class);
manager.register(double[].class);
manager.register(long[].class);
manager.register(byte[].class);
manager.register(char[].class);
manager.register(boolean[].class);
manager.register(String[].class);
manager.register(Integer[].class);
manager.register(Short[].class);
manager.register(Float[].class);
manager.register(Double[].class);
manager.register(Long[].class);
manager.register(Byte[].class);
manager.register(Character[].class);
manager.register(Boolean[].class);
manager.register(Data.class);
}
static public class Data {
public String string;
public String[] strings;
public int[] ints;
public short[] shorts;
public float[] floats;
public double[] doubles;
public long[] longs;
public byte[] bytes;
public char[] chars;
public boolean[] booleans;
public Integer[] Ints;
public Short[] Shorts;
public Float[] Floats;
public Double[] Doubles;
public Long[] Longs;
public Byte[] Bytes;
public Character[] Chars;
public Boolean[] Booleans;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(this.Booleans);
result = prime * result + Arrays.hashCode(this.Bytes);
result = prime * result + Arrays.hashCode(this.Chars);
result = prime * result + Arrays.hashCode(this.Doubles);
result = prime * result + Arrays.hashCode(this.Floats);
result = prime * result + Arrays.hashCode(this.Ints);
result = prime * result + Arrays.hashCode(this.Longs);
result = prime * result + Arrays.hashCode(this.Shorts);
result = prime * result + Arrays.hashCode(this.booleans);
result = prime * result + Arrays.hashCode(this.bytes);
result = prime * result + Arrays.hashCode(this.chars);
result = prime * result + Arrays.hashCode(this.doubles);
result = prime * result + Arrays.hashCode(this.floats);
result = prime * result + Arrays.hashCode(this.ints);
result = prime * result + Arrays.hashCode(this.longs);
result = prime * result + Arrays.hashCode(this.shorts);
result = prime * result + (this.string == null ? 0 : this.string.hashCode());
result = prime * result + Arrays.hashCode(this.strings);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Data other = (Data) obj;
if (!Arrays.equals(this.Booleans, other.Booleans)) {
return false;
}
if (!Arrays.equals(this.Bytes, other.Bytes)) {
return false;
}
if (!Arrays.equals(this.Chars, other.Chars)) {
return false;
}
if (!Arrays.equals(this.Doubles, other.Doubles)) {
return false;
}
if (!Arrays.equals(this.Floats, other.Floats)) {
return false;
}
if (!Arrays.equals(this.Ints, other.Ints)) {
return false;
}
if (!Arrays.equals(this.Longs, other.Longs)) {
return false;
}
if (!Arrays.equals(this.Shorts, other.Shorts)) {
return false;
}
if (!Arrays.equals(this.booleans, other.booleans)) {
return false;
}
if (!Arrays.equals(this.bytes, other.bytes)) {
return false;
}
if (!Arrays.equals(this.chars, other.chars)) {
return false;
}
if (!Arrays.equals(this.doubles, other.doubles)) {
return false;
}
if (!Arrays.equals(this.floats, other.floats)) {
return false;
}
if (!Arrays.equals(this.ints, other.ints)) {
return false;
}
if (!Arrays.equals(this.longs, other.longs)) {
return false;
}
if (!Arrays.equals(this.shorts, other.shorts)) {
return false;
}
if (this.string == null) {
if (other.string != null) {
return false;
}
} else if (!this.string.equals(other.string)) {
return false;
}
if (!Arrays.equals(this.strings, other.strings)) {
return false;
}
return true;
}
@Override
public String toString() {
return "Data";
}
}
}

View File

@ -1,213 +0,0 @@
/*
* Copyright 2014 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.other
import com.conversantmedia.util.concurrent.MultithreadConcurrentQueue
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.minlog.Log
import dorkbox.network.serialization.ClassRegistration
import dorkbox.network.serialization.ClassRegistration0
import dorkbox.network.serialization.ClassRegistration1
import dorkbox.network.serialization.ClassRegistration2
import dorkbox.network.serialization.ClassRegistration3
import dorkbox.network.serialization.KryoExtra
import dorkbox.util.serialization.SerializationDefaults
import kotlinx.atomicfu.atomic
class PooledSerialization {
companion object {
init {
Log.set(Log.LEVEL_ERROR)
}
}
private var initialized = atomic(false)
private val classesToRegister = mutableListOf<ClassRegistration>()
private var kryoPoolSize = 16
private val kryoInUse = atomic(0)
@Volatile
private var kryoPool = MultithreadConcurrentQueue<KryoExtra>(kryoPoolSize)
/**
* If you customize anything, you will want to register custom types before init() is called!
*/
fun init() {
// NOTE: there are problems if our serializer is THE SAME serializer used by the network stack!
// We are explicitly differet types to prevent that form happening
initialized.value = true
}
private fun initKryo(): KryoExtra {
val kryo = KryoExtra()
SerializationDefaults.register(kryo)
classesToRegister.forEach { registration ->
registration.register(kryo)
}
return kryo
}
/**
* Registers the class using the lowest, next available integer ID and the [default serializer][Kryo.getDefaultSerializer].
* If the class is already registered, the existing entry is updated with the new serializer.
*
*
* Registering a primitive also affects the corresponding primitive wrapper.
*
* Because the ID assigned is affected by the IDs registered before it, the order classes are registered is important when using this
* method.
*
* The order must be the same at deserialization as it was for serialization.
*
* This must happen before the creation of the client/server
*/
fun <T> register(clazz: Class<T>): PooledSerialization {
require(!initialized.value) { "Serialization 'register(class)' cannot happen after initialization!" }
// The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather
// with object types... EVEN IF THERE IS A SERIALIZER
require(!clazz.isInterface) { "Cannot register '${clazz}' with specified ID for serialization. It must be an implementation." }
classesToRegister.add(ClassRegistration3(clazz))
return this
}
/**
* Registers the class using the specified ID. If the ID is already in use by the same type, the old entry is overwritten. If the ID
* is already in use by a different type, an exception is thrown.
*
*
* Registering a primitive also affects the corresponding primitive wrapper.
*
* IDs must be the same at deserialization as they were for serialization.
*
* This must happen before the creation of the client/server
*
* @param id Must be >= 0. Smaller IDs are serialized more efficiently. IDs 0-8 are used by default for primitive types and String, but
* these IDs can be repurposed.
*/
fun <T> register(clazz: Class<T>, id: Int): PooledSerialization {
require(!initialized.value) { "Serialization 'register(Class, int)' cannot happen after initialization!" }
// The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather
// with object types... EVEN IF THERE IS A SERIALIZER
require(!clazz.isInterface) { "Cannot register '${clazz}' with specified ID for serialization. It must be an implementation." }
classesToRegister.add(ClassRegistration1(clazz, id))
return this
}
/**
* Registers the class using the lowest, next available integer ID and the specified serializer. If the class is already registered,
* the existing entry is updated with the new serializer.
*
*
* Registering a primitive also affects the corresponding primitive wrapper.
*
*
* Because the ID assigned is affected by the IDs registered before it, the order classes are registered is important when using this
* method. The order must be the same at deserialization as it was for serialization.
*/
@Synchronized
fun <T> register(clazz: Class<T>, serializer: Serializer<T>): PooledSerialization {
require(!initialized.value) { "Serialization 'register(Class, Serializer)' cannot happen after initialization!" }
// The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather
// with object types... EVEN IF THERE IS A SERIALIZER
require(!clazz.isInterface) { "Cannot register '${clazz.name}' with a serializer. It must be an implementation." }
classesToRegister.add(ClassRegistration0(clazz, serializer))
return this
}
/**
* Registers the class using the specified ID and serializer. If the ID is already in use by the same type, the old entry is
* overwritten. If the ID is already in use by a different type, an exception is thrown.
*
*
* Registering a primitive also affects the corresponding primitive wrapper.
*
*
* IDs must be the same at deserialization as they were for serialization.
*
* @param id Must be >= 0. Smaller IDs are serialized more efficiently. IDs 0-8 are used by default for primitive types and String, but
* these IDs can be repurposed.
*/
@Synchronized
fun <T> register(clazz: Class<T>, serializer: Serializer<T>, id: Int): PooledSerialization {
require(!initialized.value) { "Serialization 'register(Class, Serializer, int)' cannot happen after initialization!" }
// The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather
// with object types... EVEN IF THERE IS A SERIALIZER
require(!clazz.isInterface) { "Cannot register '${clazz.name}'. It must be an implementation." }
classesToRegister.add(ClassRegistration2(clazz, serializer, id))
return this
}
/**
* @return takes a kryo instance from the pool, or creates one if the pool was empty
*/
fun takeKryo(): KryoExtra {
kryoInUse.getAndIncrement()
// ALWAYS get as many as needed. We recycle them (with an auto-growing pool) to prevent too many getting created
return kryoPool.poll() ?: initKryo()
}
/**
* Returns a kryo instance to the pool for re-use later on
*/
fun returnKryo(kryo: KryoExtra) {
val kryoCount = kryoInUse.getAndDecrement()
if (kryoCount > kryoPoolSize) {
// this is CLEARLY a problem, as we have more kryos in use that our pool can support.
// This happens when we send messages REALLY fast.
//
// We fix this by increasing the size of the pool, so kryos aren't thrown away (and create a GC hit)
synchronized(kryoInUse) {
// we have a double check here on purpose. only 1 will work
if (kryoCount > kryoPoolSize) {
val oldPool = kryoPool
val oldSize = kryoPoolSize
val newSize = kryoPoolSize * 2
kryoPoolSize = newSize
kryoPool = MultithreadConcurrentQueue<KryoExtra>(kryoPoolSize)
// take all of the old kryos and put them in the new one
val array = arrayOfNulls<KryoExtra>(oldSize)
val count = oldPool.remove(array)
for (i in 0 until count) {
kryoPool.offer(array[i])
}
}
}
}
kryoPool.offer(kryo)
}
}

View File

@ -1,296 +0,0 @@
/* Copyright (c) 2008, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
import dorkbox.network.connection.Listeners;
import dorkbox.util.exceptions.SecurityException;
// NOTE: UDP is unreliable, EVEN ON LOOPBACK! So this can fail with UDP. TCP will never fail.
public
class ReconnectTest extends BaseTest {
private final AtomicInteger receivedCount = new AtomicInteger(0);
private static final Logger logger = LoggerFactory.getLogger(ReconnectTest.class.getSimpleName());
@Test
public
void socketReuseUDP() throws IOException, SecurityException {
socketReuse(false, true);
}
@Test
public
void socketReuseTCP() throws IOException, SecurityException {
socketReuse(true, false);
}
@Test
public
void socketReuseTCPUDP() throws IOException, SecurityException {
socketReuse(true, true);
}
private
void socketReuse(final boolean useTCP, final boolean useUDP) throws SecurityException, IOException {
receivedCount.set(0);
Configuration configuration = new Configuration();
configuration.host = host;
if (useTCP) {
configuration.tcpPort = tcpPort;
}
if (useUDP) {
configuration.udpPort = udpPort;
}
AtomicReference<CountDownLatch> latch = new AtomicReference<CountDownLatch>();
Server server = new Server(configuration);
addEndPoint(server);
final Listeners listeners = server.listeners();
listeners.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(Connection connection) {
if (useTCP) {
connection.send()
.TCP("-- TCP from server");
}
if (useUDP) {
connection.send()
.UDP("-- UDP from server");
}
}
});
listeners.add(new Listener.OnMessageReceived<Connection, String>() {
@Override
public
void received(Connection connection, String object) {
int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet();
logger.error("----- <S " + connection + "> " + incrementAndGet + " : " + object);
latch.get().countDown();
}
});
server.bind(false);
// ----
Client client = new Client(configuration);
addEndPoint(client);
final Listeners listeners1 = client.listeners();
listeners1.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(Connection connection) {
if (useTCP) {
connection.send()
.TCP("-- TCP from client");
}
if (useUDP) {
connection.send()
.UDP("-- UDP from client");
}
}
});
listeners1.add(new Listener.OnMessageReceived<Connection, String>() {
@Override
public
void received(Connection connection, String object) {
int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet();
logger.error("----- <C " + connection + "> " + incrementAndGet + " : " + object);
latch.get().countDown();
}
});
int latchCount = 2;
int count = 100;
int initialCount = 2;
if (useTCP && useUDP) {
initialCount += 2;
latchCount += 2;
}
try {
for (int i = 1; i < count + 1; i++) {
logger.error(".....");
latch.set(new CountDownLatch(latchCount));
try {
client.connect(5000);
} catch (IOException e) {
e.printStackTrace();
}
int retryCount = 20;
int lastRetryCount;
int target = i * initialCount;
boolean failed = false;
synchronized (receivedCount) {
while (this.receivedCount.get() != target) {
lastRetryCount = this.receivedCount.get();
try {
latch.get().await(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
// check to see if we changed at all...
if (lastRetryCount == this.receivedCount.get()) {
if (retryCount-- < 0) {
logger.error("Aborting unit test... wrong count!");
if (useUDP) {
// If TCP and UDP both fill the pipe, THERE WILL BE FRAGMENTATION and dropped UDP packets!
// it results in severe UDP packet loss and contention.
//
// http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM
// also, a google search on just "INET97/proceedings/F3/F3_1.HTM" turns up interesting problems.
// Usually it's with ISPs.
logger.error("NOTE: UDP can fail, even on loopback! See: http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM");
}
failed = true;
break;
}
} else {
retryCount = 20;
}
}
}
client.close();
logger.error(".....");
if (failed) {
break;
}
}
int specified = count * initialCount;
int received = this.receivedCount.get();
if (specified != received) {
logger.error("NOTE: UDP can fail, even on loopback! See: http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM");
}
assertEquals(specified, received);
} finally {
stopEndPoints();
waitForThreads(10);
}
}
@Test
public
void localReuse() throws SecurityException, IOException {
receivedCount.set(0);
Server server = new Server();
addEndPoint(server);
server.listeners()
.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(Connection connection) {
connection.send()
.self("-- LOCAL from server");
}
});
server.listeners()
.add(new Listener.OnMessageReceived<Connection, String>() {
@Override
public
void received(Connection connection, String object) {
int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet();
System.out.println("----- <S " + connection + "> " + incrementAndGet + " : " + object);
}
});
// ----
Client client = new Client();
addEndPoint(client);
client.listeners()
.add(new Listener.OnConnected<Connection>() {
@Override
public
void connected(Connection connection) {
connection.send()
.self("-- LOCAL from client");
}
});
client.listeners()
.add(new Listener.OnMessageReceived<Connection, String>() {
@Override
public
void received(Connection connection, String object) {
int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet();
System.out.println("----- <C " + connection + "> " + incrementAndGet + " : " + object);
}
});
server.bind(false);
int count = 10;
for (int i = 1; i < count + 1; i++) {
client.connect(5000);
int target = i * 2;
while (this.receivedCount.get() != target) {
System.out.println("----- Waiting...");
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
client.close();
}
assertEquals(count * 2, this.receivedCount.get());
stopEndPoints();
waitForThreads(10);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,3 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
rootProject.name = "Network"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2024 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,25 +15,18 @@
*/
package dorkbox.network
import dorkbox.netUtil.IPv4
import dorkbox.netUtil.IPv6
import dorkbox.netUtil.Inet4
import dorkbox.netUtil.Inet6
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.hex.toHexString
import dorkbox.network.aeron.*
import dorkbox.network.connection.*
import dorkbox.network.connection.IpInfo.Companion.IpListenType
import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace
import dorkbox.network.connection.buffer.BufferManager
import dorkbox.network.exceptions.ServerException
import dorkbox.network.handshake.ServerHandshake
import dorkbox.network.handshake.ServerHandshakePollers
import dorkbox.network.ipFilter.IpFilterRule
import dorkbox.network.rmi.RmiSupportServer
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import java.net.InetAddress
import java.util.concurrent.*
@ -47,90 +40,41 @@ import java.util.concurrent.*
* @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)
open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerConfiguration(), loggerName: String = Server::class.java.simpleName)
: EndPoint<CONNECTION>(config, loggerName) {
companion object {
/**
* Gets the version number.
*/
const val version = "5.32"
const val version = Configuration.version
/**
* Ensures that an endpoint (using the specified configuration) is NO LONGER running.
*
* NOTE: This method should only be used to check if a server is running for a DIFFERENT configuration than the currently running server
*
* By default, we will wait the [Configuration.connectionCloseTimeoutInSeconds] * 2 amount of time before returning.
*
* @return true if the media driver is STOPPED.
*/
fun ensureStopped(configuration: ServerConfiguration): Boolean {
val timeout = TimeUnit.SECONDS.toMillis(configuration.connectionCloseTimeoutInSeconds.toLong() * 2)
val logger = LoggerFactory.getLogger(Server::class.java.simpleName)
return AeronDriver.ensureStopped(configuration.copy(), logger, timeout)
}
/**
* 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
* NOTE: This method should only be used to check if a server is running for a DIFFERENT configuration than the currently running server
*
* @return true if the media driver is active and running
*/
fun isRunning(configuration: ServerConfiguration): Boolean {
return AeronDriver(configuration).isRunning()
val logger = LoggerFactory.getLogger(Server::class.java.simpleName)
return AeronDriver.isRunning(configuration.copy(), logger)
}
init {
@ -144,261 +88,310 @@ open class Server<CONNECTION : Connection>(
*/
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)
// /**
// * Maintains a thread-safe collection of rules used to define the connection type with this server.
// */
// private val connectionRules = CopyOnWriteArrayList<ConnectionRule>()
/**
* These are run in lock-step to shutdown/close the server. Afterwards, bind() can be called again
* the IP address information, if available.
*/
@Volatile
private var shutdownPollLatch = CountDownLatch(1)
internal val ipInfo = IpInfo(config)
@Volatile
private var shutdownEventLatch = CountDownLatch(1)
internal lateinit var handshake: ServerHandshake<CONNECTION>
/**
* Maintains a thread-safe collection of rules used to define the connection type with this server.
* Different connections (to the same client) can be "buffered", meaning that if they "go down" because of a network glitch -- the data
* being sent is not lost (it is buffered) and then re-sent once the new connection is established. References to the old connection
* will also redirect to the new connection.
*/
private val connectionRules = CopyOnWriteArrayList<ConnectionRule>()
internal val bufferedManager: BufferManager<CONNECTION>
/**
* 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) {
when (config.listenIpAddress) {
"loopback", "localhost", "lo", "127.0.0.1", "::1" -> IPv4.LOCALHOST
"0", "::", "0.0.0.0", "*" -> {
// this is the "wildcard" address. Windows has problems with this.
IPv4.WILDCARD
}
else -> Inet4.toAddress(config.listenIpAddress) // Inet4Address.getAllByName(config.listenIpAddress)[0]
}
}
else {
null
}
internal val listenIPv6Address: InetAddress? =
if (canUseIPv6) {
when (config.listenIpAddress) {
"loopback", "localhost", "lo", "127.0.0.1", "::1" -> IPv6.LOCALHOST
"0", "::", "0.0.0.0", "*" -> {
// this is the "wildcard" address. Windows has problems with this.
IPv6.WILDCARD
}
else -> Inet6.toAddress(config.listenIpAddress)
}
}
else {
null
}
private val string0: String by lazy {
"EndPoint [Server: ${storage.publicKey.toHexString()}]"
}
init {
// we are done with initial configuration, now finish serialization
serialization.finishInit(type)
bufferedManager = BufferManager(config, listenerManager, aeronDriver, config.bufferedConnectionTimeoutSeconds)
}
final override fun newException(message: String, cause: Throwable?): Throwable {
return ServerException(message, cause)
// +2 because we do not want to see the stack for the abstract `newException`
val serverException = ServerException(message, cause)
serverException.cleanStackTrace(2)
return serverException
}
/**
* Binds the server to AERON configuration
* Binds the server IPC only, using the previously set AERON configuration
*/
fun bindIpc() {
if (!config.enableIpc) {
logger.warn("IPC explicitly requested, but not enabled. Enabling IPC...")
// we explicitly requested IPC, make sure it's enabled
config.contextDefined = false
config.enableIpc = true
config.contextDefined = true
}
if (config.enableIPv4) { logger.warn("IPv4 is enabled, but only IPC will be used.") }
if (config.enableIPv6) { logger.warn("IPv6 is enabled, but only IPC will be used.") }
internalBind(port1 = 0, port2 = 0, onlyBindIpc = true, runShutdownCheck = true)
}
/**
* Binds the server to UDP ports, using the previously set AERON configuration
*
* @param port1 this is the network port which will be listening for incoming connections
* @param port2 this is the network port that the server will use to work around NAT firewalls. By default, this is port1+1, but
* can also be configured independently. This is required, and must be different from port1.
*/
@Suppress("DuplicatedCode")
fun bind() {
// NOTE: it is critical to remember that Aeron DOES NOT like running from coroutines!
fun bind(port1: Int, port2: Int = port1+1) {
if (config.enableIPv4 || config.enableIPv6) {
require(port1 != port2) { "port1 cannot be the same as port2" }
require(port1 > 0) { "port1 must be > 0" }
require(port2 > 0) { "port2 must be > 0" }
require(port1 < 65535) { "port1 must be < 65535" }
require(port2 < 65535) { "port2 must be < 65535" }
}
if (bindAlreadyCalled.getAndSet(true)) {
logger.error { "Unable to bind when the server is already running!" }
internalBind(port1 = port1, port2 = port2, onlyBindIpc = false, runShutdownCheck = true)
}
@Suppress("DuplicatedCode")
private fun internalBind(port1: Int, port2: Int, onlyBindIpc: Boolean, runShutdownCheck: Boolean) {
// the lifecycle of a server is the ENDPOINT (measured via the network event poller)
if (endpointIsRunning.value) {
listenerManager.notifyError(ServerException("Unable to start, the server is already running!"))
return
}
if (runShutdownCheck && !waitForEndpointShutdown()) {
listenerManager.notifyError(ServerException("Unable to start the server!"))
return
}
try {
startDriver()
} catch (e: Exception) {
logger.error(e) { "Unable to start the network driver" }
initializeState()
}
catch (e: Exception) {
resetOnError()
listenerManager.notifyError(ServerException("Unable to start the server!", e))
return
}
shutdownPollLatch = CountDownLatch(1)
shutdownEventLatch = CountDownLatch(1)
this@Server.port1 = port1
this@Server.port2 = port2
config as ServerConfiguration
val handshake = ServerHandshake(logger, config, listenerManager, aeronDriver)
// 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 ipcPoller: AeronPoller = ServerHandshakePollers.ipc(aeronDriver, config, server, handshake)
handshake = ServerHandshake(config, listenerManager, aeronDriver, eventDispatch)
// 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)
}
val ipcPoller: AeronPoller = if (config.enableIpc || onlyBindIpc) {
ServerHandshakePollers.ipc(server, handshake)
} else {
ipv4Poller = ServerHandshakePollers.ip4(aeronDriver, config, server, handshake)
ipv6Poller = ServerHandshakePollers.ip6(aeronDriver, config, server, handshake)
ServerHandshakePollers.disabled("IPC Disabled")
}
val networkEventProcessor = Runnable {
pollStartupLatch.countDown()
val ipPoller = if (onlyBindIpc) {
ServerHandshakePollers.disabled("IPv4/6 Disabled")
} else {
when (ipInfo.ipType) {
// IPv6 will bind to IPv4 wildcard as well, so don't bind both!
IpListenType.IPWildcard -> ServerHandshakePollers.ip6Wildcard(server, handshake)
IpListenType.IPv4Wildcard -> ServerHandshakePollers.ip4(server, handshake)
IpListenType.IPv6Wildcard -> ServerHandshakePollers.ip6(server, handshake)
IpListenType.IPv4 -> ServerHandshakePollers.ip4(server, handshake)
IpListenType.IPv6 -> ServerHandshakePollers.ip6(server, handshake)
IpListenType.IPC -> ServerHandshakePollers.disabled("IPv4/6 Disabled")
}
}
val pollIdleStrategy = config.pollIdleStrategy.cloneToNormal()
try {
var pollCount: Int
while (!isShutdown()) {
pollCount = 0
logger.info(ipcPoller.info)
logger.info(ipPoller.info)
// if we shutdown/close before the poller starts, we don't want to block forever
pollerClosedLatch = CountDownLatch(1)
networkEventPoller.submit(
action = object : EventActionOperator {
override fun invoke(): Int {
return if (!shutdownEventPoller) {
// 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 checks to see if there are NEW clients to handshake with
var pollCount = ipcPoller.poll() + ipPoller.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()) {
if (connection.canPoll()) {
// 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" }
if (logger.isDebugEnabled) {
logger.debug("[${connection}] connection expired (cleanup)")
}
// the connection MUST be removed in the same thread that is processing events (it will be removed again in close, and that is expected)
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
// we already removed the connection, we can call it again without side effects
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)
pollCount
} else {
// remove ourselves from processing
EventPoller.REMOVE
}
}
},
onClose = object : EventCloseOperator {
override fun invoke() {
val mustRestartDriverOnError = aeronDriver.internal.mustRestartDriverOnError
logger.debug("Server event dispatch closing...")
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()
ipPoller.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" }
// we only need to run shutdown methods if there was a network outage or D/C
if (!shutdownInProgress.value) {
// this is because we restart automatically on driver errors
this@Server.close(closeEverything = false, sendDisconnectMessage = true, releaseWaitingThreads = !mustRestartDriverOnError)
}
// 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()
if (mustRestartDriverOnError) {
logger.error("Critical driver error detected, restarting server.")
eventDispatch.CLOSE.launch {
waitForEndpointShutdown()
// also wait for everyone else to shutdown!!
aeronDriver.internal.endPointUsages.forEach {
if (it !== this@Server) {
it.waitForEndpointShutdown()
}
}
// if we restart/reconnect too fast, errors from the previous run will still be present!
aeronDriver.delayLingerTimeout()
val p1 = this@Server.port1
val p2 = this@Server.port2
if (p1 == 0 && p2 == 0) {
internalBind(port1 = 0, port2 = 0, onlyBindIpc = true, runShutdownCheck = false)
} else {
internalBind(port1 = p1, port2 = p2, onlyBindIpc = false, runShutdownCheck = false)
}
}
}
// we can now call bind again
endpointIsRunning.lazySet(false)
logger.debug("Closed the Network Event Poller task.")
pollerClosedLatch.countDown()
}
})
}
// /**
// * 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))
// }
/**
* Adds an IP+subnet rule that defines if that IP+subnet is allowed/denied connectivity to this server.
*
* By default, if there are no filter rules, then all connections are allowed to connect
* If there are filter rules - then ONLY connections for the filter that returns true are allowed to connect (all else are denied)
*
* If ANY filter rule that is applied returns true, then the connection is permitted
*
* This function will be called for **only** network clients (IPC client are excluded)
*
* @param ipFilterRule the IpFilterRule to determine if this connection will be allowed to connect
*/
fun filter(ipFilterRule: IpFilterRule) {
listenerManager.filter(ipFilterRule)
}
/**
* 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)
* Adds a function that will be called BEFORE a client/server "connects" with each other, and used to determine if a connection
* should be allowed
*
* If no rules are defined, then for LOOPBACK, it will always be `COMPRESS` and for everything else it will always be `COMPRESS_AND_ENCRYPT`.
* By default, if there are no filter rules, then all connections are allowed to connect
* If there are filter rules - then ONLY connections for the filter that returns true are allowed to connect (all else are denied)
*
* If rules are defined, then everything by default is `COMPRESS_AND_ENCRYPT`.
* It is the responsibility of the custom filter to write the error, if there is one
*
* 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
* If the function returns TRUE, then the connection will continue to connect.
* If the function returns FALSE, then the other end of the connection will
* receive a connection error
*
*
* If ANY filter rule that is applied returns true, then the connection is permitted
*
* This function will be called for **only** network clients (IPC client are excluded)
*
* @param function clientAddress: UDP connection address
* tagName: the connection tag name
*/
fun addConnectionRules(vararg rules: ConnectionRule) {
connectionRules.addAll(listOf(*rules))
fun filter(function: (clientAddress: InetAddress, tagName: String) -> Boolean) {
listenerManager.filter(function)
}
/**
* Adds a function that will be called BEFORE a client/server "connects" with each other, and used to determine if buffered messages
* for a connection should be enabled
*
* By default, if there are no rules, then all connections will have buffered messages enabled
* If there are rules - then ONLY connections for the rule that returns true will have buffered messages enabled (all else are disabled)
*
* It is the responsibility of the custom filter to write the error, if there is one
*
* If the function returns TRUE, then the buffered messages for a connection are enabled.
* If the function returns FALSE, then the buffered messages for a connection is disabled.
*
* If ANY rule that is applied returns true, then the buffered messages for a connection are enabled
*
* @param function clientAddress: not-null when UDP connection, null when IPC connection
* tagName: the connection tag name
*/
fun enableBufferedMessages(function: (clientAddress: InetAddress?, tagName: String) -> Boolean) {
listenerManager.enableBufferedMessages(function)
}
/**
@ -411,20 +404,36 @@ open class Server<CONNECTION : Connection>(
}
/**
* Closes the server and all it's connections. After a close, you may call 'bind' again.
* Will throw an exception if there are resources that are still in use
*/
final override fun close0() {
// when we call close, it will shutdown the polling mechanism, then wait for us to tell it to cleanup 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()
}
fun checkForMemoryLeaks() {
AeronDriver.checkForMemoryLeaks()
// make sure that we have de-allocated all connection data
handshake.checkForMemoryLeaks()
}
/**
* By default, if you call close() on the server, it will shut down all parts of the endpoint (listeners, driver, event polling, etc).
*
* @param closeEverything if true, all parts of the server will be closed (listeners, driver, event polling, etc)
*/
fun close(closeEverything: Boolean = true) {
bufferedManager.close()
close(closeEverything = closeEverything, sendDisconnectMessage = true, releaseWaitingThreads = true)
}
override fun toString(): String {
return string0
}
fun <R> use(block: (Server<CONNECTION>) -> R): R {
return try {
block(this)
} finally {
close()
}
}
// /**
// * Only called by the server!

View File

@ -1,36 +1,43 @@
/*
* Copyright 2023 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.aeron
import dorkbox.network.Configuration
import dorkbox.util.NamedThreadFactory
import dorkbox.network.exceptions.AeronDriverException
import dorkbox.util.Sys
import io.aeron.driver.MediaDriver
import io.aeron.exceptions.DriverTimeoutException
import mu.KLogger
import org.slf4j.Logger
import java.io.Closeable
import java.io.File
import java.util.concurrent.locks.*
class AeronContext(
val config: Configuration,
val type: Class<*> = AeronDriver::class.java,
val logger: KLogger,
aeronErrorHandler: (error: Throwable) -> Unit
) {
fun close() {
context.close()
// Destroys this thread group and all of its subgroups.
// This thread group must be empty, indicating that all threads that had been in this thread group have since stopped.
threadFactory.group.destroy()
}
import java.util.concurrent.*
/**
* Creates the Aeron Media Driver context
*
* @throws IllegalStateException if the configuration has already been used to create a context
* @throws IllegalArgumentException if the aeron media driver directory cannot be setup
*/
internal class AeronContext(config: Configuration.MediaDriverConfig, logger: Logger, aeronErrorHandler: (Throwable) -> Unit) : Closeable {
companion object {
private fun create(
config: Configuration,
threadFactory: NamedThreadFactory,
aeronErrorHandler: (error: Throwable) -> Unit
): MediaDriver.Context {
private fun create(config: Configuration.MediaDriverConfig, aeronErrorHandler: (Throwable) -> Unit): MediaDriver.Context {
// LOW-LATENCY SETTINGS
// .termBufferSparseFile(false)
// MediaDriver.Context()
// .termBufferSparseFile(false)
// .useWindowsHighResTimer(true)
// .threadingMode(ThreadingMode.DEDICATED)
// .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE)
@ -42,53 +49,69 @@ class AeronContext(
// setProperty("aeron.socket.so_rcvbuf", "2097152");
// setProperty("aeron.rcv.initial.window.length", "2097152");
val threadFactory = Configuration.aeronThreadFactory
// driver context must happen in the initializer, because we have a Server.isRunning() method that uses the mediaDriverContext (without bind)
val mediaDriverContext = MediaDriver.Context()
.termBufferSparseFile(false) // files occupy the same space virtually AND physically!
.useWindowsHighResTimer(true)
// we assign our OWN ID! so we reserve everything.
.publicationReservedSessionIdLow(AeronDriver.RESERVED_SESSION_ID_LOW)
.publicationReservedSessionIdHigh(AeronDriver.RESERVED_SESSION_ID_HIGH)
.threadingMode(config.threadingMode)
.mtuLength(config.networkMtuSize)
.ipcMtuLength(config.ipcMtuSize)
.initialWindowLength(config.initialWindowLength)
.socketSndbufLength(config.sendBufferSize)
.socketRcvbufLength(config.receiveBufferSize)
mediaDriverContext
.conductorThreadFactory(threadFactory)
.receiverThreadFactory(threadFactory)
.senderThreadFactory(threadFactory)
.sharedNetworkThreadFactory(threadFactory)
.sharedThreadFactory(threadFactory)
mediaDriverContext.aeronDirectoryName(config.aeronDirectory!!.absolutePath)
if (mediaDriverContext.ipcTermBufferLength() != io.aeron.driver.Configuration.ipcTermBufferLength()) {
// default 64 megs each is HUGE
mediaDriverContext.ipcTermBufferLength(8 * 1024 * 1024)
if (config.sendBufferSize > 0) {
mediaDriverContext.socketSndbufLength(config.sendBufferSize)
}
if (mediaDriverContext.publicationTermBufferLength() != io.aeron.driver.Configuration.termBufferLength()) {
// default 16 megs each is HUGE (we run out of space in production w/ lots of clients)
mediaDriverContext.publicationTermBufferLength(2 * 1024 * 1024)
if (config.receiveBufferSize > 0) {
mediaDriverContext.socketRcvbufLength(config.receiveBufferSize)
}
if (config.conductorIdleStrategy != null) {
mediaDriverContext.conductorIdleStrategy(config.conductorIdleStrategy)
}
if (config.sharedIdleStrategy != null) {
mediaDriverContext.sharedIdleStrategy(config.sharedIdleStrategy)
}
if (config.receiverIdleStrategy != null) {
mediaDriverContext.receiverIdleStrategy(config.receiverIdleStrategy)
}
if (config.senderIdleStrategy != null) {
mediaDriverContext.senderIdleStrategy(config.senderIdleStrategy)
}
mediaDriverContext.aeronDirectoryName(config.aeronDirectory!!.path)
if (config.ipcTermBufferLength > 0) {
mediaDriverContext.ipcTermBufferLength(config.ipcTermBufferLength)
}
if (config.publicationTermBufferLength > 0) {
mediaDriverContext.publicationTermBufferLength(config.publicationTermBufferLength)
}
// we DO NOT want to abort the JVM if there are errors.
// this replaces the default handler with one that doesn't abort the JVM
mediaDriverContext.errorHandler { error ->
aeronErrorHandler(error)
}
mediaDriverContext.errorHandler(aeronErrorHandler)
return mediaDriverContext
}
}
// this is the aeron conductor/network processor thread factory which manages the incoming messages from the network.
internal val threadFactory = NamedThreadFactory(
"Aeron",
ThreadGroup("${type.simpleName}-AeronDriver"), Thread.MAX_PRIORITY,
true)
// the context is validated before the AeronDriver object is created
val context: MediaDriver.Context
@ -105,11 +128,17 @@ class AeronContext(
*
* @return the aeron context directory
*/
val driverDirectory: File
val directory: File
get() {
return context.aeronDirectory()
}
fun deleteAeronDir(): Boolean {
// NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the
// same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense).
return directory.deleteRecursively()
}
/**
* Checks to see if an endpoint (using the specified configuration) is running.
*
@ -120,15 +149,20 @@ class AeronContext(
return context.isDriverActive(context.driverTimeoutMs()) { }
}
private fun isRunning(context: MediaDriver.Context): Boolean {
// if the media driver is running, it will be a quick connection. Usually 100ms or so
return try {
context.isDriverActive(context.driverTimeoutMs()) { }
} catch (e: Exception) {
false
}
}
/**
* Creates the Aeron Media Driver context
*
* @throws IllegalStateException if the configuration has already been used to create a context
* @throws IllegalArgumentException if the aeron media driver directory cannot be setup
*/
init {
var context = create(config, threadFactory, aeronErrorHandler)
init {
// NOTE: if a DIFFERENT PROCESS is using the SAME driver location, THERE WILL BE POTENTIAL PROBLEMS!
// ADDITIONALLY, the ONLY TIME we create a new aeron context is when it is the FIRST aeron context for a driver. Within the same
// JVM, the aeron driver/context is SHARED.
val context = create(config, aeronErrorHandler)
// this happens EXACTLY once. Must be BEFORE the "isRunning" check!
context.concludeAeronDirectory()
@ -139,57 +173,57 @@ class AeronContext(
val driverTimeout = context.driverTimeoutMs()
// sometimes when starting up, if a PREVIOUS run was corrupted (during startup, for example)
// we ONLY do this during the initial startup check because it will delete the directory, and we don't
// always want to do this.
var isRunning = try {
// we ONLY do this during the initial startup check because it will delete the directory, and we don't always want to do this.
val isRunning = try {
context.isDriverActive(driverTimeout) { }
} catch (e: DriverTimeoutException) {
// we have to delete the directory, since it was corrupted, and we try again.
if (aeronDir.deleteRecursively()) {
if (!config.forceAllowSharedAeronDriver && aeronDir.deleteRecursively()) {
context.isDriverActive(driverTimeout) { }
} else if (config.forceAllowSharedAeronDriver) {
// we are expecting a shared directory. SOMETHING is screwed up!
throw AeronDriverException("Aeron was expected to be running, and the current location is corrupted. Not doing anything!", e)
} else {
// unable to delete the directory
throw e
}
}
// this is incompatible with IPC, and will not be set if IPC is enabled
if (config.uniqueAeronDirectory && isRunning) {
val savedParent = aeronDir.parentFile
var retry = 0
val retryMax = 100
// only do this if we KNOW we are not running!
if (!isRunning) {
// NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the
// same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense).
// make sure it's clean!
aeronDir.deleteRecursively()
while (config.uniqueAeronDirectory && isRunning) {
if (retry++ > retryMax) {
throw IllegalArgumentException("Unable to force unique aeron Directory. Tried $retryMax times and all tries were in use.")
}
// if we are not CURRENTLY running, then we should ALSO delete it when we are done!
context.dirDeleteOnShutdown()
} else if (!config.forceAllowSharedAeronDriver) {
// maybe it's a mistake because we restarted too quickly! A brief pause to fix this!
val randomNum = (1..retryMax).shuffled().first()
val newDir = savedParent.resolve("${aeronDir.name}_$randomNum")
val timeoutInNs = TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong()) + context.publicationLingerTimeoutNs()
val timeoutInMs = TimeUnit.NANOSECONDS.toMillis(timeoutInNs)
logger.warn("Aeron is currently running, waiting ${Sys.getTimePrettyFull(timeoutInNs)} for it to close.")
context = create(config, threadFactory, aeronErrorHandler)
context.aeronDirectoryName(newDir.path)
// this happens EXACTLY once. Must be BEFORE the "isRunning" check!
context.concludeAeronDirectory()
isRunning = context.isDriverActive(driverTimeout) { }
// wait for it to close! wait longer.
val startTime = System.nanoTime()
while (isRunning(context) && System.nanoTime() - startTime < timeoutInNs) {
Thread.sleep(timeoutInMs)
}
if (!isRunning) {
// NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the
// same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense).
// since we are forcing a unique directory, we should ALSO delete it when we are done!
context.dirDeleteOnShutdown()
}
require(!isRunning(context)) { "Aeron is currently running, and this is the first instance created by this JVM. " +
"You must use `config.forceAllowSharedAeronDriver` to be able to re-use a shared aeron process at: $aeronDir" }
}
logger.info { "Aeron directory: '${context.aeronDirectory()}'" }
this.context = context
}
override fun toString(): String {
return context.toString()
}
override fun close() {
context.close()
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,23 @@
/*
* Copyright 2023 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.aeron
internal interface AeronPoller {
fun poll(): Int
fun close()
val info: String
}

View File

@ -1,3 +1,19 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2014-2022 Real Logic Limited.
*
@ -33,22 +49,19 @@ import java.util.*
* [StreamStat] counters.
*
*
* Each stream managed by the [io.aeron.driver.MediaDriver] will be sampled and printed out on [System.out].
*/
class BacklogStat
/**
* Construct by using a [CountersReader] which can be obtained from [Aeron.countersReader].
* Each stream managed by the [io.aeron.driver.MediaDriver] will be sampled
*
* @param counters to read for tracking positions.
*/
(private val counters: CountersReader) {
class BacklogStat(private val counters: CountersReader) {
/**
* Take a snapshot of all the backlog information and group by stream.
*
* @return a snapshot of all the backlog information and group by stream.
*/
fun snapshot(): Map<StreamCompositeKey, StreamBacklog> {
val streams: MutableMap<StreamCompositeKey, StreamBacklog> = HashMap()
val streams = mutableMapOf<StreamCompositeKey, StreamBacklog>()
counters.forEach { counterId: Int, typeId: Int, keyBuffer: DirectBuffer, _: String? ->
if (typeId >= PublisherLimit.PUBLISHER_LIMIT_TYPE_ID && typeId <= ReceiverPos.RECEIVER_POS_TYPE_ID || typeId == SenderLimit.SENDER_LIMIT_TYPE_ID || typeId == PerImageIndicator.PER_IMAGE_TYPE_ID || typeId == PublisherPos.PUBLISHER_POS_TYPE_ID) {
val key = StreamCompositeKey(

View File

@ -1,352 +0,0 @@
/*
* Copyright 2014-2020 Real Logic Limited.
*
* 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
*
* https://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.aeron
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import org.agrona.concurrent.BackoffIdleStrategy
import org.agrona.hints.ThreadHints
abstract class BackoffIdleStrategyPrePad {
var p000: Byte = 0
var p001: Byte = 0
var p002: Byte = 0
var p003: Byte = 0
var p004: Byte = 0
var p005: Byte = 0
var p006: Byte = 0
var p007: Byte = 0
var p008: Byte = 0
var p009: Byte = 0
var p010: Byte = 0
var p011: Byte = 0
var p012: Byte = 0
var p013: Byte = 0
var p014: Byte = 0
var p015: Byte = 0
var p016: Byte = 0
var p017: Byte = 0
var p018: Byte = 0
var p019: Byte = 0
var p020: Byte = 0
var p021: Byte = 0
var p022: Byte = 0
var p023: Byte = 0
var p024: Byte = 0
var p025: Byte = 0
var p026: Byte = 0
var p027: Byte = 0
var p028: Byte = 0
var p029: Byte = 0
var p030: Byte = 0
var p031: Byte = 0
var p032: Byte = 0
var p033: Byte = 0
var p034: Byte = 0
var p035: Byte = 0
var p036: Byte = 0
var p037: Byte = 0
var p038: Byte = 0
var p039: Byte = 0
var p040: Byte = 0
var p041: Byte = 0
var p042: Byte = 0
var p043: Byte = 0
var p044: Byte = 0
var p045: Byte = 0
var p046: Byte = 0
var p047: Byte = 0
var p048: Byte = 0
var p049: Byte = 0
var p050: Byte = 0
var p051: Byte = 0
var p052: Byte = 0
var p053: Byte = 0
var p054: Byte = 0
var p055: Byte = 0
var p056: Byte = 0
var p057: Byte = 0
var p058: Byte = 0
var p059: Byte = 0
var p060: Byte = 0
var p061: Byte = 0
var p062: Byte = 0
var p063: Byte = 0
}
abstract class BackoffIdleStrategyData(
protected val maxSpins: Long, protected val maxYields: Long, protected val minParkPeriodMs: Long, protected val maxParkPeriodMs: Long) : BackoffIdleStrategyPrePad() {
protected var state = 0 // NOT_IDLE
protected var spins: Long = 0
protected var yields: Long = 0
protected var parkPeriodMs: Long = 0
}
/**
* Idling strategy for threads when they have no work to do.
* <p>
* Spin for maxSpins, then
* [Coroutine.yield] for maxYields, then
* [Coroutine.delay] on an exponential backoff to maxParkPeriodMs
*/
@Suppress("unused")
class CoroutineBackoffIdleStrategy : BackoffIdleStrategyData, CoroutineIdleStrategy {
var p064: Byte = 0
var p065: Byte = 0
var p066: Byte = 0
var p067: Byte = 0
var p068: Byte = 0
var p069: Byte = 0
var p070: Byte = 0
var p071: Byte = 0
var p072: Byte = 0
var p073: Byte = 0
var p074: Byte = 0
var p075: Byte = 0
var p076: Byte = 0
var p077: Byte = 0
var p078: Byte = 0
var p079: Byte = 0
var p080: Byte = 0
var p081: Byte = 0
var p082: Byte = 0
var p083: Byte = 0
var p084: Byte = 0
var p085: Byte = 0
var p086: Byte = 0
var p087: Byte = 0
var p088: Byte = 0
var p089: Byte = 0
var p090: Byte = 0
var p091: Byte = 0
var p092: Byte = 0
var p093: Byte = 0
var p094: Byte = 0
var p095: Byte = 0
var p096: Byte = 0
var p097: Byte = 0
var p098: Byte = 0
var p099: Byte = 0
var p100: Byte = 0
var p101: Byte = 0
var p102: Byte = 0
var p103: Byte = 0
var p104: Byte = 0
var p105: Byte = 0
var p106: Byte = 0
var p107: Byte = 0
var p108: Byte = 0
var p109: Byte = 0
var p110: Byte = 0
var p111: Byte = 0
var p112: Byte = 0
var p113: Byte = 0
var p114: Byte = 0
var p115: Byte = 0
var p116: Byte = 0
var p117: Byte = 0
var p118: Byte = 0
var p119: Byte = 0
var p120: Byte = 0
var p121: Byte = 0
var p122: Byte = 0
var p123: Byte = 0
var p124: Byte = 0
var p125: Byte = 0
var p126: Byte = 0
var p127: Byte = 0
companion object {
private const val NOT_IDLE = 0
private const val SPINNING = 1
private const val YIELDING = 2
private const val PARKING = 3
/**
* Name to be returned from [.alias].
*/
const val ALIAS = "backoff"
/**
* Default number of times the strategy will spin without work before going to next state.
*/
const val DEFAULT_MAX_SPINS = 10L
/**
* Default number of times the strategy will yield without work before going to next state.
*/
const val DEFAULT_MAX_YIELDS = 5L
/**
* Default interval the strategy will park the thread on entering the park state in milliseconds.
*/
const val DEFAULT_MIN_PARK_PERIOD_MS = 1L
/**
* Default interval the strategy will park the thread will expand interval to as a max in milliseconds.
*/
const val DEFAULT_MAX_PARK_PERIOD_MS = 1000L
}
/**
* Default constructor using [.DEFAULT_MAX_SPINS], [.DEFAULT_MAX_YIELDS], [.DEFAULT_MIN_PARK_PERIOD_MS], and [.DEFAULT_MAX_PARK_PERIOD_MS].
*/
constructor() : super(DEFAULT_MAX_SPINS, DEFAULT_MAX_YIELDS, DEFAULT_MIN_PARK_PERIOD_MS, DEFAULT_MAX_PARK_PERIOD_MS) {}
/**
* Create a set of state tracking idle behavior
* <p>
* @param maxSpins to perform before moving to [Coroutine.yield]
* @param maxYields to perform before moving to [Coroutine.delay]
* @param minParkPeriodMs to use when initiating parking
* @param maxParkPeriodMs to use for end duration when parking
*/
constructor(maxSpins: Long, maxYields: Long, minParkPeriodMs: Long, maxParkPeriodMs: Long)
: super(maxSpins, maxYields, minParkPeriodMs, maxParkPeriodMs) {
}
/**
* Perform current idle action (e.g. nothing/yield/sleep). This method signature expects users to call into it on
* every work 'cycle'. The implementations may use the indication "workCount &gt; 0" to reset internal backoff
* state. This method works well with 'work' APIs which follow the following rules:
* <ul>
* <li>'work' returns a value larger than 0 when some work has been done</li>
* <li>'work' returns 0 when no work has been done</li>
* <li>'work' may return error codes which are less than 0, but which amount to no work has been done</li>
* </ul>
* <p>
* Callers are expected to follow this pattern:
*
* <pre>
* <code>
* while (isRunning)
* {
* idleStrategy.idle(doWork());
* }
* </code>
* </pre>
*
* @param workCount performed in last duty cycle.
*/
override suspend fun idle(workCount: Int) {
if (workCount > 0) {
reset()
} else {
idle()
}
}
/**
* Perform current idle action (e.g. nothing/yield/sleep). To be used in conjunction with
* {@link IdleStrategy#reset()} to clear internal state when idle period is over (or before it begins).
* Callers are expected to follow this pattern:
*
* <pre>
* <code>
* while (isRunning)
* {
* if (!hasWork())
* {
* idleStrategy.reset();
* while (!hasWork())
* {
* if (!isRunning)
* {
* return;
* }
* idleStrategy.idle();
* }
* }
* doWork();
* }
* </code>
* </pre>
*/
override suspend fun idle() {
when (state) {
NOT_IDLE -> {
state = SPINNING
spins++
}
SPINNING -> {
ThreadHints.onSpinWait()
if (++spins > maxSpins) {
state = YIELDING
yields = 0
}
}
YIELDING -> if (++yields > maxYields) {
state = PARKING
parkPeriodMs = minParkPeriodMs
} else {
yield()
}
PARKING -> {
delay(parkPeriodMs)
// double the delay until we get to MAX
parkPeriodMs = (parkPeriodMs shl 1).coerceAtMost(maxParkPeriodMs)
}
}
}
/**
* Reset the internal state in preparation for entering an idle state again.
*/
override fun reset() {
spins = 0
yields = 0
parkPeriodMs = minParkPeriodMs
state = NOT_IDLE
}
/**
* Simple name by which the strategy can be identified.
*
* @return simple name by which the strategy can be identified.
*/
override fun alias(): String {
return ALIAS
}
/**
* Creates a clone of this IdleStrategy
*/
override fun clone(): CoroutineBackoffIdleStrategy {
return CoroutineBackoffIdleStrategy(maxSpins = maxSpins, maxYields = maxYields, minParkPeriodMs = minParkPeriodMs, maxParkPeriodMs = maxParkPeriodMs)
}
/**
* Creates a clone of this IdleStrategy
*/
override fun cloneToNormal(): BackoffIdleStrategy {
return BackoffIdleStrategy(maxSpins, maxYields, minParkPeriodMs, maxParkPeriodMs)
}
override fun toString(): String {
return "BackoffIdleStrategy{" +
"alias=" + ALIAS +
", maxSpins=" + maxSpins +
", maxYields=" + maxYields +
", minParkPeriodMs=" + minParkPeriodMs +
", maxParkPeriodMs=" + maxParkPeriodMs +
'}'
}
}

View File

@ -1,117 +0,0 @@
/*
* Copyright 2014-2020 Real Logic Limited.
*
* 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
*
* https://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.aeron
import org.agrona.concurrent.IdleStrategy
/**
* Idle strategy for use by threads when they do not have work to do.
*
*
* **Note regarding implementor state**
*
*
* Some implementations are known to be stateful, please note that you cannot safely assume implementations to be
* stateless. Where implementations are stateful it is recommended that implementation state is padded to avoid false
* sharing.
*
*
* **Note regarding potential for TTSP(Time To Safe Point) issues**
*
*
* If the caller spins in a 'counted' loop, and the implementation does not include a a safepoint poll this may cause a
* TTSP (Time To SafePoint) problem. If this is the case for your application you can solve it by preventing the idle
* method from being inlined by using a Hotspot compiler command as a JVM argument e.g:
* `-XX:CompileCommand=dontinline,org.agrona.concurrent.NoOpIdleStrategy::idle`
*/
interface CoroutineIdleStrategy {
/**
* Perform current idle action (e.g. nothing/yield/sleep). This method signature expects users to call into it on
* every work 'cycle'. The implementations may use the indication "workCount &gt; 0" to reset internal backoff
* state. This method works well with 'work' APIs which follow the following rules:
* <ul>
* <li>'work' returns a value larger than 0 when some work has been done</li>
* <li>'work' returns 0 when no work has been done</li>
* <li>'work' may return error codes which are less than 0, but which amount to no work has been done</li>
* </ul>
* <p>
* Callers are expected to follow this pattern:
*
* <pre>
* <code>
* while (isRunning)
* {
* idleStrategy.idle(doWork());
* }
* </code>
* </pre>
*
* @param workCount performed in last duty cycle.
*/
suspend fun idle(workCount: Int)
/**
* Perform current idle action (e.g. nothing/yield/sleep). To be used in conjunction with
* {@link IdleStrategy#reset()} to clear internal state when idle period is over (or before it begins).
* Callers are expected to follow this pattern:
*
* <pre>
* <code>
* while (isRunning)
* {
* if (!hasWork())
* {
* idleStrategy.reset();
* while (!hasWork())
* {
* if (!isRunning)
* {
* return;
* }
* idleStrategy.idle();
* }
* }
* doWork();
* }
* </code>
* </pre>
*/
suspend fun idle()
/**
* Reset the internal state in preparation for entering an idle state again.
*/
fun reset()
/**
* Simple name by which the strategy can be identified.
*
* @return simple name by which the strategy can be identified.
*/
fun alias(): String {
return ""
}
/**
* Creates a clone of this IdleStrategy
*/
fun clone(): CoroutineIdleStrategy
/**
* Creates a clone of this IdleStrategy
*/
fun cloneToNormal(): IdleStrategy
}

View File

@ -1,108 +0,0 @@
/*
* Copyright 2014-2020 Real Logic Limited.
*
* 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
*
* https://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.aeron
import kotlinx.coroutines.delay
import org.agrona.concurrent.SleepingMillisIdleStrategy
/**
* When idle this strategy is to sleep for a specified period time in milliseconds.
*
*
* This class uses [Coroutine.delay] to idle.
*/
class CoroutineSleepingMillisIdleStrategy : CoroutineIdleStrategy {
companion object {
/**
* Name to be returned from [.alias].
*/
const val ALIAS = "sleep-ms"
/**
* Default sleep period when the default constructor is used.
*/
const val DEFAULT_SLEEP_PERIOD_MS = 1L
}
private val sleepPeriodMs: Long
/**
* Default constructor that uses [.DEFAULT_SLEEP_PERIOD_MS].
*/
constructor() {
sleepPeriodMs = DEFAULT_SLEEP_PERIOD_MS
}
/**
* Constructed a new strategy that will sleep for a given period when idle.
*
* @param sleepPeriodMs period in milliseconds for which the strategy will sleep when work count is 0.
*/
constructor(sleepPeriodMs: Long) {
this.sleepPeriodMs = sleepPeriodMs
}
/**
* {@inheritDoc}
*/
override suspend fun idle(workCount: Int) {
if (workCount > 0) {
return
}
delay(sleepPeriodMs)
}
/**
* {@inheritDoc}
*/
override suspend fun idle() {
delay(sleepPeriodMs)
}
/**
* {@inheritDoc}
*/
override fun reset() {}
/**
* {@inheritDoc}
*/
override fun alias(): String {
return ALIAS
}
/**
* Creates a clone of this IdleStrategy
*/
override fun clone(): CoroutineSleepingMillisIdleStrategy {
return CoroutineSleepingMillisIdleStrategy(sleepPeriodMs = sleepPeriodMs)
}
/**
* Creates a clone of this IdleStrategy
*/
override fun cloneToNormal(): SleepingMillisIdleStrategy {
return SleepingMillisIdleStrategy(sleepPeriodMs)
}
override fun toString(): String {
return "SleepingMillisIdleStrategy{" +
"alias=" + ALIAS +
", sleepPeriodMs=" + sleepPeriodMs +
'}'
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,17 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DuplicatedCode")
package dorkbox.network.aeron.mediaDriver
package dorkbox.network.aeron
import io.aeron.Subscription
abstract class MediaDriverServer(val port: Int,
val streamId: Int,
val sessionId: Int,
val connectionTimeoutSec: Int, val
isReliable: Boolean) : MediaDriverConnection {
lateinit var subscription: Subscription
internal interface EventActionOperator {
operator fun invoke(): Int
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2023 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.aeron
internal interface EventCloseOperator {
operator fun invoke()
}

View File

@ -0,0 +1,241 @@
/*
* Copyright 2024 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.aeron
import dorkbox.bytes.ByteArrayWrapper
import dorkbox.collections.ConcurrentIterator
import dorkbox.network.Configuration
import dorkbox.network.connection.EndPoint
import dorkbox.util.NamedThreadFactory
import kotlinx.atomicfu.atomic
import org.agrona.concurrent.IdleStrategy
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.concurrent.*
import java.util.concurrent.locks.*
import kotlin.concurrent.write
/**
* there are threading issues if there are client(s) and server's within the same JVM, where we have thread starvation
*
* additionally, if we have MULTIPLE clients on the same machine, we are limited by the CPU core count. Ideally we want to share
* this among ALL clients within the same JVM so that we can support multiple clients/servers
*/
internal class EventPoller {
private class EventAction(val onAction: EventActionOperator, val onClose: EventCloseOperator)
companion object {
internal const val REMOVE = -1
val eventLogger = LoggerFactory.getLogger(EventPoller::class.java.simpleName)
private val pollExecutor = Executors.newSingleThreadExecutor(
NamedThreadFactory("Poll Dispatcher", Configuration.networkThreadGroup, true)
)
}
private var configured = false
private lateinit var pollStrategy: IdleStrategy
@Volatile
private var running = false
private var lock = ReentrantReadWriteLock()
// this is thread safe
private val pollEvents = ConcurrentIterator<EventAction>()
private val submitEvents = atomic(0)
private val configureEventsEndpoints = mutableSetOf<ByteArrayWrapper>()
@Volatile
private var shutdownLatch = CountDownLatch(0)
@Volatile
private var threadId = 0L
fun isDispatch(): Boolean {
// this only works because we are a single thread dispatch
return threadId == Thread.currentThread().id
}
fun configure(logger: Logger, config: Configuration, endPoint: EndPoint<*>) {
lock.write {
if (logger.isDebugEnabled) {
logger.debug("Initializing the Network Event Poller...")
}
configureEventsEndpoints.add(ByteArrayWrapper.wrap(endPoint.storage.publicKey))
if (!configured) {
if (logger.isTraceEnabled) {
logger.trace("Configuring the Network Event Poller...")
}
running = true
configured = true
shutdownLatch = CountDownLatch(1)
pollStrategy = config.pollIdleStrategy
pollExecutor.submit {
val pollIdleStrategy = pollStrategy
var pollCount = 0
threadId = Thread.currentThread().id // only ever 1 thread!!!
pollIdleStrategy.reset()
while (running) {
pollEvents.forEachRemovable {
try {
// check to see if we should remove this event (when a client/server closes, it is removed)
// once ALL endpoint are closed, this is shutdown.
val poll = it.onAction()
// <0 means we remove the event from processing
// 0 means we idle
// >0 means reset and don't idle (because there are likely more poll events)
if (poll < 0) {
// remove our event, it is no longer valid
pollEvents.remove(this)
it.onClose() // shutting down
} else if (poll > 0) {
pollCount += poll
}
} catch (e: Exception) {
eventLogger.error("Unexpected error during Network Event Polling! Aborting event dispatch for it!", e)
// remove our event, it is no longer valid
pollEvents.remove(this)
it.onClose() // shutting down
}
}
pollIdleStrategy.idle(pollCount)
}
// now we have to REMOVE all poll events -- so that their remove logic will run.
pollEvents.forEachRemovable {
// remove our event, it is no longer valid
pollEvents.remove(this)
it.onClose() // shutting down
}
shutdownLatch.countDown()
}
} else {
// we don't want to use .equals, because that also compares STATE, which for us is going to be different because we are cloned!
// toString has the right info to compare types/config accurately
require(pollStrategy.toString() == config.pollIdleStrategy.toString()) {
"The network event poll strategy is different between the multiple instances of network clients/servers. There **WILL BE** thread starvation, so this behavior is forbidden!"
}
}
}
}
/**
* Will cause the executing thread to wait until the event has been started
*/
fun submit(action: EventActionOperator, onClose: EventCloseOperator) = lock.write {
submitEvents.getAndIncrement()
// this forces the current thread to WAIT until the network poll system has started
val pollStartupLatch = CountDownLatch(1)
pollEvents.add(EventAction(action, onClose))
pollEvents.add(EventAction(
object : EventActionOperator {
override fun invoke(): Int {
pollStartupLatch.countDown()
// remove ourselves
return REMOVE
}
}
, object : EventCloseOperator {
override fun invoke() {}
}
))
pollStartupLatch.await()
submitEvents.getAndDecrement()
}
/**
* Waits for all events to finish running
*/
fun close(logger: Logger, endPoint: EndPoint<*>) {
// make sure that we close on the CLOSE dispatcher if we run on the poll dispatcher!
if (isDispatch()) {
endPoint.eventDispatch.CLOSE.launch {
close(logger, endPoint)
}
return
}
lock.write {
logger.debug("Requesting close for the Network Event Poller...")
// ONLY if there are no more poll-events do we ACTUALLY shut down.
// when an endpoint closes its polling, it will automatically be removed from this datastructure.
val publicKeyWrapped = ByteArrayWrapper.wrap(endPoint.storage.publicKey)
configureEventsEndpoints.removeIf { it == publicKeyWrapped }
val cEvents = configureEventsEndpoints.size
// these prevent us from closing too early
val pEvents = pollEvents.size()
val sEvents = submitEvents.value
if (running && sEvents == 0 && cEvents == 0) {
when (pEvents) {
0 -> {
logger.debug("Closing the Network Event Poller...")
doClose(logger)
}
else -> {
if (logger.isDebugEnabled) {
logger.debug("Not closing the Network Event Poller... (isRunning=$running submitEvents=$sEvents configureEvents=${cEvents} pollEvents=$pEvents)")
}
}
}
} else if (logger.isDebugEnabled) {
logger.debug("Not closing the Network Event Poller... (isRunning=$running submitEvents=$sEvents configureEvents=${cEvents} pollEvents=$pEvents)")
}
}
}
private fun doClose(logger: Logger) {
val wasRunning = running
running = false
while (!shutdownLatch.await(500, TimeUnit.MILLISECONDS)) {
logger.error("Waiting for Network Event Poller to close. It should not take this long")
}
configured = false
if (wasRunning) {
pollExecutor.awaitTermination(200, TimeUnit.MILLISECONDS)
}
logger.debug("Closed Network Event Poller: wasRunning=$wasRunning")
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright 2023 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.aeron
import dorkbox.jna.ClassUtils
import dorkbox.os.OS
import javassist.ClassPool
import javassist.CtNewMethod
object FixTransportPoller {
// allow access to sun.nio.ch.SelectorImpl without causing reflection or JPMS module issues
fun init() {
if (OS.javaVersion <= 11) {
// older versions of java don't need to worry about rewriting anything
return
}
try {
val pool = ClassPool.getDefault()
run {
val dynamicClass = pool.makeClass("sun.nio.ch.SelectorImplAccessory")
val method = CtNewMethod.make(
("public static java.lang.reflect.Field getKey(java.lang.String fieldName) { " +
"java.lang.reflect.Field field = Class.forName(\"sun.nio.ch.SelectorImpl\").getDeclaredField( fieldName );" +
"field.setAccessible( true );" +
"return field;" +
"}"), dynamicClass
)
dynamicClass.addMethod(method)
val dynamicClassBytes = dynamicClass.toBytecode()
ClassUtils.defineClass(null, dynamicClassBytes)
}
// have to trampoline off this to get around module access
run {
val dynamicClass = pool.makeClass("java.lang.SelectorImplAccessory")
val method = CtNewMethod.make(
("public static java.lang.reflect.Field getKey(java.lang.String fieldName) { " +
"return sun.nio.ch.SelectorImplAccessory.getKey(fieldName);" +
"}"), dynamicClass
)
dynamicClass.addMethod(method)
val dynamicClassBytes = dynamicClass.toBytecode()
ClassUtils.defineClass(null, dynamicClassBytes)
}
run {
val dynamicClass = pool.getCtClass("org.agrona.nio.TransportPoller")
// Get the static initializer
val staticInitializer = dynamicClass.classInitializer
// Remove the existing static initializer
dynamicClass.removeConstructor(staticInitializer)
val initializer = dynamicClass.makeClassInitializer()
initializer.insertAfter(
"java.lang.System.err.println(\"updating TransportPoller!\");" +
"java.lang.reflect.Field selectKeysField = null;\n" +
"java.lang.reflect.Field publicSelectKeysField = null;\n" +
"try {\n" +
" java.nio.channels.Selector selector = java.nio.channels.Selector.open();\n" +
" Throwable var3 = null;\n" + "\n" +
" try {\n" +
" Class clazz = Class.forName(\"sun.nio.ch.SelectorImpl\", false, ClassLoader.getSystemClassLoader());\n" +
" if (clazz.isAssignableFrom(selector.getClass())) {\n" +
" selectKeysField = java.lang.SelectorImplAccessory.getKey(\"selectedKeys\");\n" +
" publicSelectKeysField = java.lang.SelectorImplAccessory.getKey(\"publicSelectedKeys\");\n" +
" }\n" +
" } catch (Throwable var21) {\n" +
" var3 = var21;\n" +
" throw var21;\n" +
" } finally {\n" +
" if (selector != null) {\n" +
" if (var3 != null) {\n" +
" try {\n" +
" selector.close();\n" +
" } catch (Throwable var20) {\n" +
" var3.addSuppressed(var20);\n" +
" }\n" +
" } else {\n" +
" selector.close();\n" +
" }\n" +
" }\n" +
" }\n" +
"} catch (Exception var23) {\n" +
" org.agrona.LangUtil.rethrowUnchecked(var23);\n" +
"} finally {\n" +
" org.agrona.nio.TransportPoller.SELECTED_KEYS_FIELD = selectKeysField;\n" +
" org.agrona.nio.TransportPoller.PUBLIC_SELECTED_KEYS_FIELD = publicSelectKeysField;\n" +
"}"
)
// perform pre-verification for the modified method
initializer.methodInfo.rebuildStackMapForME(pool)
val dynamicClassBytes = dynamicClass.toBytecode()
ClassUtils.defineClass(ClassLoader.getSystemClassLoader(), dynamicClassBytes)
}
} catch (e: Exception) {
throw RuntimeException("Could not fix Aeron TransportPoller", e)
}
}
}

View File

@ -1,118 +0,0 @@
/*
* Copyright 2021 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.aeron.mediaDriver
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.mediaDriver.MediaDriverConnection.Companion.uri
import dorkbox.network.connection.ListenerManager
import dorkbox.network.exceptions.ClientRetryException
import dorkbox.network.exceptions.ClientTimedOutException
import mu.KLogger
import java.lang.Thread.sleep
import java.util.concurrent.*
/**
* For a client, the streamId specified here MUST be manually flipped because they are in the perspective of the SERVER
* NOTE: IPC connection will ALWAYS have a timeout of 10 second to connect. This is IPC, it should connect fast
*/
internal open class ClientIpcDriver(streamId: Int,
sessionId: Int,
localSessionId: Int) :
MediaDriverClient(
port = streamId,
streamId = streamId,
remoteSessionId = sessionId,
localSessionId = localSessionId,
connectionTimeoutSec = 10,
isReliable = true
) {
var success: Boolean = false
override val type = "ipc"
override val subscriptionPort: Int = localSessionId
/**
* Set up the subscription + publication channels to the server
*
* @throws ClientRetryException if we need to retry to connect
* @throws ClientTimedOutException if we cannot connect to the server in the designated time
*/
fun build(aeronDriver: AeronDriver, logger: KLogger) {
// Create a publication at the given address and port, using the given stream ID.
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
val publicationUri = uri("ipc", remoteSessionId)
// Create a subscription at the given address and port, using the given stream ID.
val subscriptionUri = uri("ipc", 0)
if (logger.isTraceEnabled) {
logger.trace("IPC client pub URI: ${publicationUri.build()}")
logger.trace("IPC server sub URI: ${subscriptionUri.build()}")
}
var success = false
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
// For publications, if we add them "too quickly" (faster than the 'linger' timeout), Aeron will throw exceptions.
// ESPECIALLY if it is with the same streamID
// this check is in the "reconnect" logic
val publication = aeronDriver.addPublication(publicationUri, streamId)
val subscription = aeronDriver.addSubscription(subscriptionUri, localSessionId)
// always include the linger timeout, so we don't accidentally kill ourself by taking too long
val timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong()) + aeronDriver.getLingerNs()
val startTime = System.nanoTime()
while (System.nanoTime() - startTime < timoutInNanos) {
if (publication.isConnected) {
success = true
break
}
sleep(500L)
}
if (!success) {
subscription.close()
publication.close()
val clientTimedOutException = ClientTimedOutException("Cannot create publication IPC connection to server")
ListenerManager.cleanAllStackTrace(clientTimedOutException)
throw clientTimedOutException
}
this.success = true
this.subscription = subscription
this.publication = publication
}
override val info : String by lazy {
if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) {
"[$sessionId] IPC connection established to [$streamId|$subscriptionPort]"
} else {
"Connecting handshake to IPC [$streamId|$subscriptionPort]"
}
}
override fun toString(): String {
return info
}
}

View File

@ -1,146 +0,0 @@
/*
* Copyright 2021 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.aeron.mediaDriver
import dorkbox.netUtil.IPv4
import dorkbox.netUtil.IPv6
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.mediaDriver.MediaDriverConnection.Companion.uriEndpoint
import dorkbox.network.connection.ListenerManager
import dorkbox.network.exceptions.ClientRetryException
import dorkbox.network.exceptions.ClientTimedOutException
import mu.KLogger
import java.lang.Thread.sleep
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.util.concurrent.*
/**
* For a client, the ports specified here MUST be manually flipped because they are in the perspective of the SERVER.
* A connection timeout of 0, means to wait forever
*/
internal class ClientUdpDriver(val address: InetAddress, val addressString: String,
port: Int,
streamId: Int,
sessionId: Int,
localSessionId: Int,
connectionTimeoutSec: Int = 0,
isReliable: Boolean) :
MediaDriverClient(port, streamId, sessionId, localSessionId, connectionTimeoutSec, isReliable) {
var success: Boolean = false
override val type: String by lazy {
if (address is Inet4Address) {
"IPv4"
} else {
"IPv6"
}
}
override val subscriptionPort: Int by lazy {
val addressesAndPorts = subscription.localSocketAddresses()
val first = addressesAndPorts.first()
// split
val splitPoint = first.lastIndexOf(':')
val port = first.substring(splitPoint+1)
port.toInt()
}
/**
* @throws ClientRetryException if we need to retry to connect
* @throws ClientTimedOutException if we cannot connect to the server in the designated time
*/
@Suppress("DuplicatedCode")
fun build(aeronDriver: AeronDriver, logger: KLogger) {
var success = false
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
// on close, the publication CAN linger (in case a client goes away, and then comes back)
// AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param)
// Create a publication at the given address and port, using the given stream ID.
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
val publicationUri = uriEndpoint("udp", remoteSessionId, isReliable, address, addressString, port)
logger.trace("client pub URI: $type ${publicationUri.build()}")
// For publications, if we add them "too quickly" (faster than the 'linger' timeout), Aeron will throw exceptions.
// ESPECIALLY if it is with the same streamID. This was noticed as a problem with IPC
val publication = aeronDriver.addPublication(publicationUri, streamId)
val localAddresses = publication.localSocketAddresses().first()
// split
val splitPoint = localAddresses.lastIndexOf(':')
val localAddressString = localAddresses.substring(0, splitPoint)
// the subscription here is WILDCARD
val localAddress = if (address is Inet6Address) {
IPv6.toAddress(localAddressString)!!
} else {
IPv4.toAddress(localAddressString)!!
}
// Create a subscription the given address and port, using the given stream ID.
val subscriptionUri = uriEndpoint("udp", localSessionId, isReliable, localAddress, localAddressString, 0)
logger.trace("client sub URI: $type ${subscriptionUri.build()}")
val subscription = aeronDriver.addSubscription(subscriptionUri, streamId)
// always include the linger timeout, so we don't accidentally kill ourself by taking too long
val timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong()) + aeronDriver.getLingerNs()
val startTime = System.nanoTime()
while (System.nanoTime() - startTime < timoutInNanos) {
if (publication.isConnected) {
success = true
break
}
sleep(500L)
}
if (!success) {
subscription.close()
publication.close()
val ex = ClientTimedOutException("Cannot create publication to $type $addressString in $connectionTimeoutSec seconds")
ListenerManager.cleanAllStackTrace(ex)
throw ex
}
this.success = true
this.publication = publication
this.subscription = subscription
}
override val info: String by lazy {
if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) {
"$addressString [$port|$subscriptionPort] [$streamId|$sessionId] (reliable:$isReliable)"
} else {
"Connecting handshake to $addressString [$port|$subscriptionPort] [$streamId|*] (reliable:$isReliable)"
}
}
override fun toString(): String {
return info
}
}

View File

@ -1,34 +0,0 @@
/*
* 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.
*/
@file:Suppress("DuplicatedCode")
package dorkbox.network.aeron.mediaDriver
import io.aeron.Publication
import io.aeron.Subscription
abstract class MediaDriverClient(val port: Int,
val streamId: Int,
val remoteSessionId: Int,
val localSessionId: Int,
val connectionTimeoutSec: Int,
val isReliable: Boolean) : MediaDriverConnection {
lateinit var subscription: Subscription
lateinit var publication: Publication
abstract val subscriptionPort: Int
}

View File

@ -1,16 +0,0 @@
package dorkbox.network.aeron.mediaDriver
import io.aeron.Publication
import io.aeron.Subscription
import java.net.InetAddress
data class MediaDriverConnectInfo(val subscription: Subscription,
val publication: Publication,
val subscriptionPort: Int,
val publicationPort: Int,
val streamId: Int,
val sessionId: Int,
val isReliable: Boolean,
val remoteAddress: InetAddress?,
val remoteAddressString: String,
)

View File

@ -1,65 +0,0 @@
/*
* 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.
*/
@file:Suppress("DuplicatedCode")
package dorkbox.network.aeron.mediaDriver
import dorkbox.network.aeron.AeronDriver
import io.aeron.ChannelUriStringBuilder
import java.net.Inet4Address
import java.net.InetAddress
interface MediaDriverConnection {
val type: String
// We don't use 'suspend' for these, because we have to pump events from a NORMAL thread. If there are any suspend points, there is
// the potential for a live-lock due to coroutine scheduling
val info : String
companion object {
fun uri(type: String, sessionId: Int, isReliable: Boolean? = null): ChannelUriStringBuilder {
val builder = ChannelUriStringBuilder().media(type)
if (isReliable != null) {
builder.reliable(isReliable)
}
if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) {
builder.sessionId(sessionId)
}
return builder
}
fun uriEndpoint(type: String, sessionId: Int, isReliable: Boolean, address: InetAddress, addressString: String, port: Int): ChannelUriStringBuilder {
val builder = uri(type, sessionId, isReliable)
if (address is Inet4Address) {
builder.endpoint("$addressString:$port")
} else {
// IPv6 requires the address to be bracketed by [...]
if (addressString[0] == '[') {
builder.endpoint("$addressString:$port")
} else {
// there MUST be [] surrounding the IPv6 address for aeron to like it!
builder.endpoint("[$addressString]:$port")
}
}
return builder
}
}
}

View File

@ -1,63 +0,0 @@
/*
* Copyright 2021 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.aeron.mediaDriver
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.mediaDriver.MediaDriverConnection.Companion.uri
import mu.KLogger
/**
* For a client, the streamId specified here MUST be manually flipped because they are in the perspective of the SERVER
* NOTE: IPC connection will ALWAYS have a timeout of 10 second to connect. This is IPC, it should connect fast
*/
internal open class ServerIpcDriver(streamId: Int,
sessionId: Int) :
MediaDriverServer(0, streamId, sessionId, 10, true) {
var success: Boolean = false
override val type = "ipc"
/**
* Setup the subscription + publication channels on the server.
*
* serverAddress is ignored for IPC
*/
fun build(aeronDriver: AeronDriver, logger: KLogger) {
// Create a subscription at the given address and port, using the given stream ID.
val subscriptionUri = uri("ipc", sessionId)
if (logger.isTraceEnabled) {
logger.trace("IPC server sub URI: ${subscriptionUri.build()}")
}
success = true
subscription = aeronDriver.addSubscription(subscriptionUri, streamId)
}
override val info : String by lazy {
if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) {
"[$sessionId] IPC listening on [$streamId] [$sessionId]"
} else {
"Listening handshake on IPC [$streamId] [$sessionId]"
}
}
override fun toString(): String {
return info
}
}

View File

@ -1,81 +0,0 @@
/*
* Copyright 2021 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.aeron.mediaDriver
import dorkbox.netUtil.IP
import dorkbox.netUtil.IPv4
import dorkbox.netUtil.IPv6
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.mediaDriver.MediaDriverConnection.Companion.uriEndpoint
import mu.KLogger
import java.net.Inet4Address
import java.net.InetAddress
/**
* For a client, the ports specified here MUST be manually flipped because they are in the perspective of the SERVER.
* A connection timeout of 0, means to wait forever
*/
internal open class ServerUdpDriver(val listenAddress: InetAddress,
port: Int,
streamId: Int,
sessionId: Int,
connectionTimeoutSec: Int,
isReliable: Boolean) :
MediaDriverServer(port, streamId, sessionId, connectionTimeoutSec, isReliable) {
var success: Boolean = false
override val type = "udp"
fun build(aeronDriver: AeronDriver, logger: KLogger) {
// Create a subscription at the given address and port, using the given stream ID.
val subscriptionUri = uriEndpoint("udp", sessionId, isReliable, listenAddress, IP.toString(listenAddress), port)
if (logger.isTraceEnabled) {
if (listenAddress is Inet4Address) {
logger.trace("IPV4 server sub URI: ${subscriptionUri.build()}")
} else {
logger.trace("IPV6 server sub URI: ${subscriptionUri.build()}")
}
}
this.success = true
this.subscription = aeronDriver.addSubscription(subscriptionUri, streamId)
}
override val info: String by lazy {
val address = if (listenAddress == IPv4.WILDCARD || listenAddress == IPv6.WILDCARD) {
if (listenAddress == IPv4.WILDCARD) {
listenAddress.hostAddress
} else {
IPv4.WILDCARD.hostAddress + "/" + listenAddress.hostAddress
}
} else {
IP.toString(listenAddress)
}
if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) {
"Listening on $address [$port] [$streamId|$sessionId] (reliable:$isReliable)"
} else {
"Listening handshake on $address [$port] [$streamId|*] (reliable:$isReliable)"
}
}
override fun toString(): String {
return info
}
}

View File

@ -1,43 +0,0 @@
/*
* Copyright 2021 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.aeron.mediaDriver
import io.aeron.Publication
import java.net.InetAddress
/**
* This represents the connection PAIR between a server<->client
* A connection timeout of 0, means to wait forever
*/
internal class UdpMediaDriverPairedConnection(
listenAddress: InetAddress,
val remoteAddress: InetAddress,
val remoteAddressString: String,
val publicationPort: Int,
subscriptionPort: Int,
streamId: Int,
sessionId: Int,
connectionTimeoutSec: Int,
isReliable: Boolean,
val publication: Publication
) :
ServerUdpDriver(listenAddress, subscriptionPort, streamId, sessionId, connectionTimeoutSec, isReliable) {
override fun toString(): String {
return "$remoteAddressString [$port|$publicationPort] [$streamId|$sessionId] (reliable:$isReliable)"
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 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.aeron;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,61 +15,102 @@
*/
package dorkbox.network.connection
import dorkbox.network.handshake.ConnectionCounts
import dorkbox.network.handshake.RandomId65kAllocator
import dorkbox.network.Client
import dorkbox.network.Server
import dorkbox.network.aeron.AeronDriver.Companion.sessionIdAllocator
import dorkbox.network.aeron.AeronDriver.Companion.streamIdAllocator
import dorkbox.network.connection.buffer.BufferedMessages
import dorkbox.network.connection.buffer.BufferedSession
import dorkbox.network.ping.Ping
import dorkbox.network.ping.PingManager
import dorkbox.network.rmi.RmiSupportConnection
import io.aeron.FragmentAssembler
import io.aeron.Publication
import io.aeron.Subscription
import io.aeron.Image
import io.aeron.logbuffer.FragmentHandler
import io.aeron.logbuffer.Header
import io.aeron.protocol.DataHeaderFlyweight
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.getAndUpdate
import kotlinx.coroutines.runBlocking
import org.agrona.DirectBuffer
import java.lang.Thread.sleep
import java.net.InetAddress
import java.util.concurrent.*
import javax.crypto.SecretKey
/**
* This connection is established once the registration information is validated, and the various connect/filter checks have passed
* This connection is established once the registration information is validated, and the various connect/filter checks have passed.
*
* Connections are also BUFFERED, meaning that if the connection between a client-server goes down because of a network glitch, then the
* data being sent is not lost (it is buffered) and then re-sent once a new connection has the same UUID within the timout period.
*
* References to the old connection will also redirect to the new connection.
*/
open class Connection(connectionParameters: ConnectionParams<*>) {
private var messageHandler: FragmentAssembler
internal val subscription: Subscription
internal val publication: Publication
private val messageHandler: FragmentHandler
/**
* The publication port (used by aeron) for this connection. This is from the perspective of the server!
* The specific connection details for this connection!
*
* NOTE: remember, the connection details are for the connection, but the toString() info is reversed for the client
* (so that we can line-up client/server connection logs)
*/
private val subscriptionPort: Int
private val publicationPort: Int
val info = connectionParameters.connectionInfo
/**
* the stream id of this connection. Can be 0 for IPC connections
* the endpoint associated with this connection
*/
val streamId: Int
internal val endPoint = connectionParameters.endPoint
internal val subscription = info.sub
internal val publication = info.pub
private lateinit var image: Image
// only accessed on a single thread!
private val connectionExpirationTimoutNanos = endPoint.config.connectionExpirationTimoutNanos
// the timeout starts from when the connection is first created, so that we don't get "instant" timeouts when the server rejects a connection
private var connectionTimeoutTimeNanos = System.nanoTime()
/**
* the session id of this connection. This value is UNIQUE
* There can be concurrent writes to the network stack, at most 1 per connection. Each connection has its own logic on the remote endpoint,
* and can have its own back-pressure.
*/
val id: Int
internal val sendIdleStrategy = endPoint.config.sendIdleStrategy
/**
* the remote address, as a string. Will be null for IPC connections
* This is the client UUID. This is useful determine if the same client is connecting multiple times to a server (instead of only using IP address)
*/
val remoteAddress: InetAddress?
val uuid = connectionParameters.publicKey
/**
* the remote address, as a string. Will be "ipc" for IPC connections
* The unique session id of this connection, assigned by the server.
*
* Specifically this is the subscription session ID for the server
*/
val remoteAddressString: String
val id = if (endPoint::class.java == Client::class.java) {
info.sessionIdPub
} else {
info.sessionIdSub
}
/**
* The tag name for a connection permits an INCOMING client to define a custom string. The max length is 32
*/
val tag = info.tagName
/**
* The remote address, as a string. Will be null for IPC connections
*/
val remoteAddress = info.remoteAddress
/**
* The remote address, as a string. Will be "IPC" for IPC connections
*/
val remoteAddressString = info.remoteAddressString
/**
* The remote port. Will be 0 for IPC connections
*/
val remotePort = info.portPub
/**
* @return true if this connection is an IPC connection
*/
val isIpc = connectionParameters.connectionInfo.remoteAddress == null
val isIpc = info.isIpc
/**
* @return true if this connection is a network connection
@ -77,9 +118,26 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
val isNetwork = !isIpc
/**
* the endpoint associated with this connection
* used when the connection is buffered
*/
internal val endPoint = connectionParameters.endPoint
private val bufferedSession: BufferedSession
/**
* used to determine if this connection will have buffered messages enabled or not.
*/
internal val enableBufferedMessages = connectionParameters.enableBufferedMessages
/**
* The largest size a SINGLE message via AERON can be. Because the maximum size we can send in a "single fragment" is the
* publication.maxPayloadLength() function (which is the MTU length less header). We could depend on Aeron for fragment reassembly,
* but that has a (very low) maximum reassembly size -- so we have our own mechanism for object fragmentation/assembly, which
* is (in reality) only limited by available ram.
*/
internal val maxMessageSize = if (isNetwork) {
endPoint.config.networkMtuSize - DataHeaderFlyweight.HEADER_LENGTH
} else {
endPoint.config.ipcMtuSize - DataHeaderFlyweight.HEADER_LENGTH
}
private val listenerManager = atomic<ListenerManager<Connection>?>(null)
@ -87,105 +145,120 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
private val isClosed = atomic(false)
// enableNotifyDisconnect : we don't always want to enable notifications on disconnect
internal var closeAction: suspend () -> Unit = {}
// only accessed on a single thread!
private var connectionLastCheckTimeNanos = 0L
private var connectionTimeoutTimeNanos = 0L
// always offset by the linger amount, since we cannot act faster than the linger for adding/removing publications
private val connectionCheckIntervalNanos = connectionParameters.endPoint.config.connectionCheckIntervalNanos + endPoint.aeronDriver.getLingerNs()
private val connectionExpirationTimoutNanos = connectionParameters.endPoint.config.connectionExpirationTimoutNanos + endPoint.aeronDriver.getLingerNs()
// while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error.
private val remoteKeyChanged = connectionParameters.publicKeyValidation == PublicKeyValidationState.TAMPERED
// The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 8 (external counter) + 4 (GCM counter)
// The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
// counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
// private val aes_gcm_iv = atomic(0)
internal val remoteKeyChanged = connectionParameters.publicKeyValidation == PublicKeyValidationState.TAMPERED
/**
* Methods supporting Remote Method Invocation and Objects
*/
val rmi: RmiSupportConnection<out Connection>
// a record of how many messages are in progress of being sent. When closing the connection, this number must be 0
private val messagesInProgress = atomic(0)
// we customize the toString() value for this connection, and it's just better to cache it's value (since it's a modestly complex string)
// we customize the toString() value for this connection, and it's just better to cache its value (since it's a modestly complex string)
private val toString0: String
/**
* @return the AES key
*/
internal val cryptoKey: SecretKey = connectionParameters.cryptoKey
// The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter)
// The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
// counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
internal val aes_gcm_iv = atomic(0)
// Used to track that this connection WILL be closed, but has not yet been closed.
@Volatile
internal var closeRequested = false
init {
val connectionInfo = connectionParameters.connectionInfo
// NOTE: subscriptions (ie: reading from buffers, etc) are not thread safe! Because it is ambiguous HOW EXACTLY they are unsafe,
// we exclusively read from the DirectBuffer on a single thread.
id = connectionInfo.sessionId // NOTE: this is UNIQUE per server!
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be:
// - long running
// - re-entrant with the client
messageHandler = FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
// Subscriptions are NOT multi-thread safe, so only processed on the thread that calls .poll()!
endPoint.dataReceive(buffer, offset, length, header, this@Connection)
}
subscription = connectionInfo.subscription
publication = connectionInfo.publication
// can only get this AFTER we have built the sub/pub
streamId = connectionInfo.streamId // NOTE: this is UNIQUE per server!
subscriptionPort = connectionInfo.subscriptionPort
publicationPort = connectionInfo.publicationPort
remoteAddress = connectionInfo.remoteAddress
remoteAddressString = connectionInfo.remoteAddressString
toString0 = "[${id}/${streamId}] $remoteAddressString [$publicationPort|$subscriptionPort]"
messageHandler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
// this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe!
// NOTE: subscriptions (ie: reading from buffers, etc) are not thread safe! Because it is ambiguous HOW EXACTLY they are unsafe,
// we exclusively read from the DirectBuffer on a single thread.
endPoint.processMessage(buffer, offset, length, header, this@Connection)
bufferedSession = when (endPoint) {
is Server -> endPoint.bufferedManager.onConnect(this)
is Client -> endPoint.bufferedManager!!.onConnect(this)
else -> throw RuntimeException("Unable to determine type, aborting!")
}
@Suppress("LeakingThis")
rmi = connectionParameters.endPoint.rmiConnectionSupport.getNewRmiSupport(this)
rmi = endPoint.rmiConnectionSupport.getNewRmiSupport(this)
// For toString() and logging
toString0 = info.getLogInfo(logger.isDebugEnabled)
}
/**
* @return true if the remote public key changed. This can be useful if specific actions are necessary when the key has changed.
* When this is called, we should always have a subscription image!
*/
fun hasRemoteKeyChanged(): Boolean {
return remoteKeyChanged
internal fun setImage() {
var triggered = false
while (subscription.hasNoImages()) {
triggered = true
Thread.sleep(50)
}
if (triggered) {
logger.error("Delay while configuring subscription!")
}
image = subscription.imageAtIndex(0)
}
// /**
// * This is the per-message sequence number.
// *
// * The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter)
// * The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
// * counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
// */
// fun nextGcmSequence(): Long {
// return aes_gcm_iv.getAndIncrement()
// }
//
// /**
// * @return the AES key. key=32 byte, iv=12 bytes (AES-GCM implementation).
// */
// fun cryptoKey(): SecretKey {
// TODO()
//// return channelWrapper.cryptoKey()
// }
/**
* Polls the AERON media driver subscription channel for incoming messages
*/
internal fun poll(): Int {
// NOTE: regarding fragment limit size. Repeated calls to '.poll' will reassemble a fragment.
// `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)`
return subscription.poll(messageHandler, 1)
return image.poll(messageHandler, 1)
}
/**
* Safely sends objects to a destination, if `abortEarly` is true, there are no retries if sending the message fails.
*
* @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown!
*/
internal fun send(message: Any, abortEarly: Boolean): Boolean {
if (logger.isTraceEnabled) {
// The handshake sessionId IS NOT globally unique
// don't automatically create the lambda when trace is disabled! Because this uses 'outside' scoped info, it's a new lambda each time!
if (logger.isTraceEnabled) {
logger.trace("[$toString0] send: ${message.javaClass.simpleName} : $message")
}
}
val success = endPoint.write(message, publication, sendIdleStrategy, this@Connection, maxMessageSize, abortEarly)
return if (!success && message !is DisconnectMessage) {
// queue up the messages, because we couldn't write them for whatever reason!
// NEVER QUEUE THE DISCONNECT MESSAGE!
bufferedSession.queueMessage(this@Connection, message, abortEarly)
} else {
success
}
}
private fun sendNoBuffer(message: Any): Boolean {
if (logger.isTraceEnabled) {
// The handshake sessionId IS NOT globally unique
// don't automatically create the lambda when trace is disabled! Because this uses 'outside' scoped info, it's a new lambda each time!
if (logger.isTraceEnabled) {
logger.trace("[$toString0] send: ${message.javaClass.simpleName} : $message")
}
}
return endPoint.write(message, publication, sendIdleStrategy, this@Connection, maxMessageSize, false)
}
/**
@ -194,11 +267,20 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
* @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown!
*/
fun send(message: Any): Boolean {
messagesInProgress.getAndIncrement()
val success = endPoint.send(message, publication, this)
messagesInProgress.getAndDecrement()
return send(message, false)
}
return success
/**
* Safely sends objects to a destination, where the callback is notified once the remote endpoint has received the message.
*
* This is to guarantee happens-before, and using this will depend upon APP+NETWORK latency, and is (by design) not as performant as
* sending a regular message!
*
* @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown!
*/
fun send(message: Any, onSuccessCallback: Connection.() -> Unit): Boolean {
return sendSync(message, onSuccessCallback)
}
/**
@ -206,17 +288,19 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
*
* @return true if the message was successfully sent by aeron
*/
suspend fun ping(pingTimeoutSeconds: Int = PingManager.DEFAULT_TIMEOUT_SECONDS, function: suspend Ping.() -> Unit): Boolean {
return endPoint.ping(this, pingTimeoutSeconds, function)
fun ping(function: Ping.() -> Unit = {}): Boolean {
return sendPing(function)
}
/**
* A message in progress means that we have requested to to send an object over the network, but it hasn't finished sending over the network
* This is the per-message sequence number.
*
* @return the number of messages in progress for this connection.
* The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter)
* The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
* counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
*/
fun messagesInProgress(): Int {
return messagesInProgress.value
internal fun nextGcmSequence(): Int {
return aes_gcm_iv.getAndIncrement()
}
/**
@ -229,10 +313,10 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
* (via connection.addListener), meaning that ONLY that listener attached to
* the connection is notified on that event (ie, admin type listeners)
*/
suspend fun onDisconnect(function: suspend Connection.() -> Unit) {
fun onDisconnect(function: Connection.() -> Unit) {
// make sure we atomically create the listener manager, if necessary
listenerManager.getAndUpdate { origManager ->
origManager ?: ListenerManager(logger)
origManager ?: ListenerManager(logger, endPoint.eventDispatch)
}
listenerManager.value!!.onDisconnect(function)
@ -241,10 +325,10 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
/**
* Adds a function that will be called only for this connection, when a client/server receives a message
*/
suspend fun <MESSAGE> onMessage(function: suspend Connection.(MESSAGE) -> Unit) {
fun <MESSAGE> onMessage(function: Connection.(MESSAGE) -> Unit) {
// make sure we atomically create the listener manager, if necessary
listenerManager.getAndUpdate { origManager ->
origManager ?: ListenerManager(logger)
origManager ?: ListenerManager(logger, endPoint.eventDispatch)
}
listenerManager.value!!.onMessage(function)
@ -255,43 +339,67 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
*
* This is ALWAYS called on a new dispatch
*/
internal suspend fun notifyOnMessage(message: Any): Boolean {
internal fun notifyOnMessage(message: Any): Boolean {
return listenerManager.value?.notifyOnMessage(this, message) ?: false
}
internal fun sendBufferedMessages() {
if (enableBufferedMessages) {
val bufferedMessage = BufferedMessages()
val numberDrained = bufferedSession.pendingMessagesQueue.drainTo(bufferedMessage.messages)
if (numberDrained > 0) {
// now send all buffered/pending messages
if (logger.isDebugEnabled) {
logger.debug("Sending buffered messages: ${bufferedSession.pendingMessagesQueue.size}")
}
sendNoBuffer(bufferedMessage)
}
}
}
/**
* @return true if this connection has had close() called
*/
fun isClosed(): Boolean {
return isClosed.value
}
/**
* Is this a "dirty" disconnect, meaning that it has timed out, but not been explicitly closed
*/
internal fun isDirtyClose(): Boolean {
return !closeRequested && !isClosed() && isClosedWithTimeout()
}
/**
* Is this connection considered still safe for polling (or rather, has it been closed in an unusual way?)
*/
internal fun canPoll(): Boolean {
return !closeRequested && !isClosed() && !isClosedWithTimeout()
}
/**
* We must account for network blips. The blips will be recovered by aeron, but we want to make sure that we are actually
* disconnected for a set period of time before we start the close process for a connection
*
* @return `true` if this connection has been closed via aeron
*/
fun isClosedViaAeron(): Boolean {
internal fun isClosedWithTimeout(): Boolean {
// we ONLY want to actually, legit check, 1 time every XXX ms.
val now = System.nanoTime()
if (now - connectionLastCheckTimeNanos < connectionCheckIntervalNanos) {
// we haven't waited long enough for another check. always return false (true means we are closed)
return false
}
connectionLastCheckTimeNanos = now
// as long as we are connected, we reset the state, so that if there is a network blip, we want to make sure that it is
// a network blip for a while, instead of just once or twice. (which can happen)
// a network blip for a while, instead of just once or twice. (which WILL happen)
if (subscription.isConnected && publication.isConnected) {
// reset connection timeout
connectionTimeoutTimeNanos = 0L
connectionTimeoutTimeNanos = now
// we are still connected (true means we are closed)
return false
}
//
// aeron is not connected
//
if (connectionTimeoutTimeNanos == 0L) {
connectionTimeoutTimeNanos = now
}
// make sure that our "isConnected" state lasts LONGER than the expiry timeout!
@ -300,92 +408,154 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
return now - connectionTimeoutTimeNanos >= connectionExpirationTimoutNanos
}
/**
* Closes the connection, and removes all connection specific listeners
*/
fun close() {
close(enableRemove = true)
close(sendDisconnectMessage = true,
closeEverything = true)
}
/**
* Closes the connection, and removes all connection specific listeners
*/
internal fun close(enableRemove: Boolean) {
internal fun close(sendDisconnectMessage: Boolean, closeEverything: Boolean) {
// there are 2 ways to call close.
// MANUALLY
// When a connection is disconnected via a timeout/expire.
// the compareAndSet is used to make sure that if we call close() MANUALLY, (and later) when the auto-cleanup/disconnect is called -- it doesn't
// try to do it again.
closeRequested = true
// make sure that EVERYTHING before "close()" runs before we do.
// If there are multiple clients/servers sharing the same NetworkPoller -- then they will wait on each other!
val close = endPoint.eventDispatch.CLOSE
if (!close.isDispatch()) {
close.launch {
close(sendDisconnectMessage = sendDisconnectMessage, closeEverything = closeEverything)
}
return
}
closeImmediately(sendDisconnectMessage = sendDisconnectMessage, closeEverything = closeEverything)
}
// connection.close() -> this
// endpoint.close() -> connection.close() -> this
internal fun closeImmediately(sendDisconnectMessage: Boolean, closeEverything: Boolean) {
// the server 'handshake' connection info is cleaned up with the disconnect via timeout/expire.
if (isClosed.compareAndSet(expect = false, update = true)) {
val aeronLogInfo = "${id}/${streamId}"
logger.debug {"[$aeronLogInfo] connection closing"}
if (!isClosed.compareAndSet(expect = false, update = true)) {
logger.debug("[$toString0] connection ignoring close request.")
return
}
subscription.close()
if (logger.isDebugEnabled) {
logger.debug("[$toString0] connection closing. sendDisconnectMessage=$sendDisconnectMessage, closeEverything=$closeEverything")
}
// send out a "close" message. MAYBE it gets to the remote endpoint, maybe not. If it DOES, then the remote endpoint starts
// the close process faster.
try {
endPoint.send(CloseMessage(), publication, this)
} catch (ignored: Exception) {
// make sure to save off the RMI objects for session management
if (!closeEverything) {
when (endPoint) {
is Server -> endPoint.bufferedManager.onDisconnect(this)
is Client -> endPoint.bufferedManager!!.onDisconnect(this)
else -> throw RuntimeException("Unable to determine type, aborting!")
}
}
val timoutInNanos = TimeUnit.SECONDS.toNanos(endPoint.config.connectionCloseTimeoutInSeconds.toLong())
var closeTimeoutTime = System.nanoTime()
// we do not want to close until AFTER all publications have been sent. Calling this WITHOUT waiting will instantly stop everything
// we want a timeout-check, otherwise this will run forever
while (messagesInProgress.value != 0 && System.nanoTime() - closeTimeoutTime < timoutInNanos) {
sleep(50)
if (!closeEverything) {
when (endPoint) {
is Server -> endPoint.bufferedManager.onDisconnect(this)
is Client -> endPoint.bufferedManager!!.onDisconnect(this)
else -> throw RuntimeException("Unable to determine type, aborting!")
}
}
// on close, we want to make sure this file is DELETED!
val logFile = endPoint.aeronDriver.getMediaDriverPublicationFile(publication.registrationId())
publication.close()
// on close, we want to make sure this file is DELETED!
try {
// we might not be able to close this connection!!
endPoint.aeronDriver.close(subscription, toString0)
}
catch (e: Exception) {
endPoint.listenerManager.notifyError(e)
}
closeTimeoutTime = System.nanoTime()
while (logFile.exists() && System.nanoTime() - closeTimeoutTime < timoutInNanos) {
if (logFile.delete()) {
break
// notify the remote endPoint that we are closing
// we send this AFTER we close our subscription (so that no more messages will be received, when the remote end ping-pong's this message back)
if (sendDisconnectMessage) {
if (publication.isConnected) {
if (logger.isDebugEnabled) {
logger.debug("Sending disconnect message to ${endPoint.otherTypeName}")
}
sleep(100)
}
if (logFile.exists()) {
logger.error("[$aeronLogInfo] Unable to delete aeron publication log on close: $logFile")
}
// sometimes the remote end has already disconnected, THERE WILL BE ERRORS if this happens (but they are ok)
if (closeEverything) {
send(DisconnectMessage.CLOSE_EVERYTHING, true)
} else {
send(DisconnectMessage.CLOSE_SIMPLE, true)
}
if (enableRemove) {
endPoint.removeConnection(this)
// wait for .5 seconds to (help) make sure that the messages are sent before shutdown! This is not guaranteed!
if (logger.isDebugEnabled) {
logger.debug("Waiting for disconnect message to send")
}
Thread.sleep(500L)
} else {
if (logger.isDebugEnabled) {
logger.debug("Publication is not connected with ${endPoint.otherTypeName}, not sending disconnect message.")
}
}
}
// NOTE: notifyDisconnect() is called inside closeAction()!!
// on close, we want to make sure this file is DELETED!
try {
// we might not be able to close this connection.
endPoint.aeronDriver.close(publication, toString0)
}
catch (e: Exception) {
endPoint.listenerManager.notifyError(e)
}
// This is set by the client/server 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
runBlocking {
closeAction()
// NOTE: any waiting RMI messages that are in-flight will terminate when they time-out (and then do nothing)
// if there are errors within the driver, we do not want to notify disconnect, as we will automatically reconnect.
endPoint.listenerManager.notifyDisconnect(this)
endPoint.removeConnection(this)
val connection = this
if (endPoint.isServer()) {
// clean up the resources associated with this connection when it's closed
if (logger.isDebugEnabled) {
logger.debug("[${connection}] freeing resources")
}
logger.debug {"[$aeronLogInfo] connection closed"}
sessionIdAllocator.free(info.sessionIdPub)
sessionIdAllocator.free(info.sessionIdSub)
streamIdAllocator.free(info.streamIdPub)
streamIdAllocator.free(info.streamIdSub)
if (remoteAddress != null) {
// unique for UDP endpoints
(endPoint as Server).handshake.connectionsPerIpCounts.decrementSlow(remoteAddress)
}
}
if (logger.isDebugEnabled) {
logger.debug("[$toString0] connection closed")
}
}
// called in postCloseAction(), so we don't expose our internal listenerManager
internal suspend fun doNotifyDisconnect() {
// called in a ListenerManager.notifyDisconnect(), so we don't expose our internal listenerManager
internal fun notifyDisconnect() {
val connectionSpecificListenerManager = listenerManager.value
connectionSpecificListenerManager?.notifyDisconnect(this@Connection)
connectionSpecificListenerManager?.directNotifyDisconnect(this@Connection)
}
//
//
// Generic object methods
//
//
override fun toString(): String {
return toString0
}
@ -409,17 +579,93 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
return id == other1.id
}
// cleans up the connection information
internal fun cleanup(connectionsPerIpCounts: ConnectionCounts, sessionIdAllocator: RandomId65kAllocator, streamIdAllocator: RandomId65kAllocator) {
sessionIdAllocator.free(id)
internal fun receiveSendSync(sendSync: SendSync) {
if (sendSync.message != null) {
// this is on the "remote end".
sendSync.message = null
if (isIpc) {
streamIdAllocator.free(publicationPort)
streamIdAllocator.free(subscriptionPort)
if (!send(sendSync)) {
logger.error("Error returning send-sync: $sendSync")
}
} else {
// unique for UDP endpoints
connectionsPerIpCounts.decrementSlow(remoteAddress!!)
streamIdAllocator.free(streamId)
// this is on the "local end" when the response comes back
val responseId = sendSync.id
// process the ping message so that our ping callback does something
// this will be null if the ping took longer than XXX seconds and was cancelled
val result = EndPoint.responseManager.removeWaiterCallback<Connection.() -> Unit>(responseId, logger)
if (result != null) {
result(this)
} else {
logger.error("Unable to receive send-sync, there was no waiting response for $sendSync ($responseId)")
}
}
}
/**
* Safely sends objects to a destination, the callback is notified once the remote endpoint has received the message.
*
* This is to guarantee happens-before, and using this will depend upon APP+NETWORK latency, and is (by design) not as performant as
* sending a regular message!
*
* @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown!
*/
private fun sendSync(message: Any, onSuccessCallback: Connection.() -> Unit): Boolean {
val id = EndPoint.responseManager.prepWithCallback(logger, onSuccessCallback)
val sendSync = SendSync()
sendSync.message = message
sendSync.id = id
// if there is no sync response EVER, it means that the connection is in a critically BAD state!
// eventually, all the ping/sync replies (or, in our case, the replies that have timed out) will
// become recycled.
// Is it a memory-leak? No, because the memory will **EVENTUALLY** get freed.
return send(sendSync, false)
}
internal fun receivePing(ping: Ping) {
if (ping.pongTime == 0L) {
// this is on the "remote end".
ping.pongTime = System.currentTimeMillis()
if (!send(ping)) {
logger.error("Error returning ping: $ping")
}
} else {
// this is on the "local end" when the response comes back
ping.finishedTime = System.currentTimeMillis()
val responseId = ping.packedId
// process the ping message so that our ping callback does something
// this will be null if the ping took longer than XXX seconds and was cancelled
val result = EndPoint.responseManager.removeWaiterCallback<Ping.() -> Unit>(responseId, logger)
if (result != null) {
result(ping)
} else {
logger.error("Unable to receive ping, there was no waiting response for $ping ($responseId)")
}
}
}
private fun sendPing(function: Ping.() -> Unit): Boolean {
val id = EndPoint.responseManager.prepWithCallback(logger, function)
val ping = Ping()
ping.packedId = id
ping.pingTime = System.currentTimeMillis()
// if there is no ping response EVER, it means that the connection is in a critically BAD state!
// eventually, all the ping replies (or, in our case, the RMI replies that have timed out) will
// become recycled.
// Is it a memory-leak? No, because the memory will **EVENTUALLY** get freed.
return send(ping)
}
}

View File

@ -1,78 +0,0 @@
/*
* 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.connection
import dorkbox.collections.ConcurrentEntry
import dorkbox.collections.ConcurrentIterator
import dorkbox.collections.ConcurrentIterator.headREF
// .equals() compares the identity on purpose,this because we cannot create two separate objects that are somehow equal to each other.
@Suppress("UNCHECKED_CAST")
internal open class ConnectionManager<CONNECTION: Connection>() {
private val connections = ConcurrentIterator<CONNECTION>()
/**
* Invoked when aeron successfully connects to a remote address.
*
* @param connection the connection to add
*/
fun add(connection: CONNECTION) {
connections.add(connection)
}
/**
* Removes a custom connection to the server.
*
* This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and
* you want *this* server instance to manage listeners + message dispatch
*
* @param connection the connection to remove
*/
fun remove(connection: CONNECTION) {
connections.remove(connection)
}
/**
* Performs an action on each connection in the list.
*/
inline fun forEach(function: (connection: CONNECTION) -> Unit) {
// access a snapshot (single-writer-principle)
val head = headREF.get(connections) as ConcurrentEntry<CONNECTION>?
var current: ConcurrentEntry<CONNECTION>? = head
var connection: CONNECTION
while (current != null) {
// Concurrent iteration...
connection = current.value
current = current.next()
function(connection)
}
}
fun connectionCount(): Int {
return connections.size()
}
/**
* Removes all connections. Does not call close or anything else on them
*/
fun clear() {
connections.clear()
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,10 +15,14 @@
*/
package dorkbox.network.connection
import dorkbox.network.aeron.mediaDriver.MediaDriverConnectInfo
import dorkbox.network.handshake.PubSub
import javax.crypto.spec.SecretKeySpec
data class ConnectionParams<CONNECTION : Connection>(
val publicKey: ByteArray,
val endPoint: EndPoint<CONNECTION>,
val connectionInfo: MediaDriverConnectInfo,
val publicKeyValidation: PublicKeyValidationState
val connectionInfo: PubSub,
val publicKeyValidation: PublicKeyValidationState,
val enableBufferedMessages: Boolean,
val cryptoKey: SecretKeySpec
)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,13 +16,13 @@
package dorkbox.network.connection
import dorkbox.bytes.Hash
import dorkbox.bytes.toHexString
import dorkbox.hex.toHexString
import dorkbox.network.handshake.ClientConnectionInfo
import dorkbox.network.serialization.AeronInput
import dorkbox.network.serialization.AeronOutput
import dorkbox.network.serialization.SettingsStore
import dorkbox.util.entropy.Entropy
import mu.KLogger
import org.slf4j.Logger
import java.math.BigInteger
import java.net.InetAddress
import java.security.KeyFactory
@ -42,33 +42,39 @@ import javax.crypto.spec.SecretKeySpec
/**
* Management for all the crypto stuff used
*/
internal class CryptoManagement(val logger: KLogger,
internal class CryptoManagement(val logger: Logger,
private val settingsStore: SettingsStore,
type: Class<*>,
private val enableRemoteSignatureValidation: Boolean) {
companion object {
private val X25519 = "X25519"
const val curve25519 = "curve25519"
const val GCM_IV_LENGTH_BYTES = 12 // 12 bytes for a 96-bit IV
const val GCM_TAG_LENGTH_BITS = 128
const val AES_ALGORITHM = "AES/GCM/NoPadding"
val NOCRYPT = SecretKeySpec(ByteArray(1), "NOCRYPT")
val secureRandom = SecureRandom()
}
private val X25519 = "X25519"
private val X25519KeySpec = NamedParameterSpec(X25519)
private val keyFactory = KeyFactory.getInstance(X25519) // key size is 32 bytes (256 bits)
private val keyAgreement = KeyAgreement.getInstance("XDH")
private val aesCipher = Cipher.getInstance("AES/GCM/NoPadding")
private val aesCipher = Cipher.getInstance(AES_ALGORITHM)
companion object {
const val curve25519 = "curve25519"
const val GCM_IV_LENGTH_BYTES = 12
const val GCM_TAG_LENGTH_BITS = 128
}
val privateKey: XECPrivateKey
val publicKey: XECPublicKey
// These are both 32 bytes long (256 bits)
val privateKeyBytes: ByteArray
val publicKeyBytes: ByteArray
val secureRandom = SecureRandom(settingsStore.getSalt())
private val iv = ByteArray(GCM_IV_LENGTH_BYTES)
val cryptOutput = AeronOutput()
val cryptInput = AeronInput()
@ -78,12 +84,17 @@ internal class CryptoManagement(val logger: KLogger,
logger.warn("WARNING: Disabling remote key validation is a security risk!!")
}
secureRandom.setSeed(settingsStore.salt)
// initialize the private/public keys used for negotiating ECC handshakes
// these are ONLY used for IP connections. LOCAL connections do not need a handshake!
var privateKeyBytes = settingsStore.getPrivateKey()
var publicKeyBytes = settingsStore.getPublicKey()
val privateKeyBytes: ByteArray
val publicKeyBytes: ByteArray
if (privateKeyBytes == null || publicKeyBytes == null) {
if (settingsStore.validKeys()) {
privateKeyBytes = settingsStore.privateKey
publicKeyBytes = settingsStore.publicKey
} else {
try {
// seed our RNG based off of this and create our ECC keys
val seedBytes = Entropy["There are no ECC keys for the ${type.simpleName} yet"]
@ -98,8 +109,8 @@ internal class CryptoManagement(val logger: KLogger,
privateKeyBytes = xdhPrivate.scalar
// save to properties file
settingsStore.savePrivateKey(privateKeyBytes)
settingsStore.savePublicKey(publicKeyBytes)
settingsStore.privateKey = privateKeyBytes
settingsStore.publicKey = publicKeyBytes
} catch (e: Exception) {
val message = "Unable to initialize/generate ECC keys. FORCED SHUTDOWN."
logger.error(message, e)
@ -107,14 +118,12 @@ internal class CryptoManagement(val logger: KLogger,
}
}
publicKeyBytes!!
logger.info("ECC public key: ${publicKeyBytes.toHexString()}")
this.publicKey = keyFactory.generatePublic(XECPublicKeySpec(X25519KeySpec, BigInteger(publicKeyBytes))) as XECPublicKey
this.privateKey = keyFactory.generatePrivate(XECPrivateKeySpec(X25519KeySpec, privateKeyBytes)) as XECPrivateKey
this.privateKeyBytes = privateKeyBytes!!
this.privateKeyBytes = privateKeyBytes
this.publicKeyBytes = publicKeyBytes
}
@ -167,10 +176,77 @@ internal class CryptoManagement(val logger: KLogger,
return PublicKeyValidationState.VALID
}
private fun makeInfo(serverPublicKeyBytes: ByteArray, secretKey: SecretKeySpec): ClientConnectionInfo {
val sessionIdPub = cryptInput.readInt()
val sessionIdSub = cryptInput.readInt()
val streamIdPub = cryptInput.readInt()
val streamIdSub = cryptInput.readInt()
val regDetailsSize = cryptInput.readInt()
val sessionTimeout = cryptInput.readLong()
val bufferedMessages = cryptInput.readBoolean()
val regDetails = cryptInput.readBytes(regDetailsSize)
// now save data off
return ClientConnectionInfo(
sessionIdPub = sessionIdPub,
sessionIdSub = sessionIdSub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
publicKey = serverPublicKeyBytes,
sessionTimeout = sessionTimeout,
bufferedMessages = bufferedMessages,
kryoRegistrationDetails = regDetails,
secretKey = secretKey)
}
// NOTE: ALWAYS CALLED ON THE SAME THREAD! (from the server, mutually exclusive calls to decrypt)
fun nocrypt(
sessionIdPub: Int,
sessionIdSub: Int,
streamIdPub: Int,
streamIdSub: Int,
sessionTimeout: Long,
bufferedMessages: Boolean,
kryoRegDetails: ByteArray
): ByteArray {
return try {
// now create the byte array that holds all our data
cryptOutput.reset()
cryptOutput.writeInt(sessionIdPub)
cryptOutput.writeInt(sessionIdSub)
cryptOutput.writeInt(streamIdPub)
cryptOutput.writeInt(streamIdSub)
cryptOutput.writeInt(kryoRegDetails.size)
cryptOutput.writeLong(sessionTimeout)
cryptOutput.writeBoolean(bufferedMessages)
cryptOutput.writeBytes(kryoRegDetails)
cryptOutput.toBytes()
} catch (e: Exception) {
logger.error("Error during AES encrypt", e)
ByteArray(0)
}
}
// NOTE: ALWAYS CALLED ON THE SAME THREAD! (from the client, mutually exclusive calls to encrypt)
fun nocrypt(registrationData: ByteArray, serverPublicKeyBytes: ByteArray): ClientConnectionInfo? {
return try {
// The message was intended for this client. Try to parse it as one of the available message types.
// this message is NOT-ENCRYPTED!
cryptInput.buffer = registrationData
makeInfo(serverPublicKeyBytes, NOCRYPT)
} catch (e: Exception) {
logger.error("Error during IPC decrypt!", e)
null
}
}
/**
* Generate the AES key based on ECDH
*/
private fun generateAesKey(remotePublicKeyBytes: ByteArray, bytesA: ByteArray, bytesB: ByteArray): SecretKeySpec {
internal fun generateAesKey(remotePublicKeyBytes: ByteArray, bytesA: ByteArray, bytesB: ByteArray): SecretKeySpec {
val clientPublicKey = keyFactory.generatePublic(XECPublicKeySpec(X25519KeySpec, BigInteger(remotePublicKeyBytes)))
keyAgreement.init(privateKey)
keyAgreement.doPhase(clientPublicKey, true)
@ -187,25 +263,32 @@ internal class CryptoManagement(val logger: KLogger,
}
// NOTE: ALWAYS CALLED ON THE SAME THREAD! (from the server, mutually exclusive calls to decrypt)
fun encrypt(clientPublicKeyBytes: ByteArray,
subscriptionPort: Int,
connectionSessionId: Int,
connectionStreamId: Int,
kryoRegDetails: ByteArray): ByteArray {
fun encrypt(
cryptoSecretKey: SecretKeySpec,
sessionIdPub: Int,
sessionIdSub: Int,
streamIdPub: Int,
streamIdSub: Int,
sessionTimeout: Long,
bufferedMessages: Boolean,
kryoRegDetails: ByteArray
): ByteArray {
try {
val secretKeySpec = generateAesKey(clientPublicKeyBytes, clientPublicKeyBytes, publicKeyBytes)
secureRandom.nextBytes(iv)
val gcmParameterSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv)
aesCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec)
aesCipher.init(Cipher.ENCRYPT_MODE, cryptoSecretKey, gcmParameterSpec)
// now create the byte array that holds all our data
cryptOutput.reset()
cryptOutput.writeInt(connectionSessionId)
cryptOutput.writeInt(connectionStreamId)
cryptOutput.writeInt(subscriptionPort)
cryptOutput.writeInt(sessionIdPub)
cryptOutput.writeInt(sessionIdSub)
cryptOutput.writeInt(streamIdPub)
cryptOutput.writeInt(streamIdSub)
cryptOutput.writeInt(kryoRegDetails.size)
cryptOutput.writeLong(sessionTimeout)
cryptOutput.writeBoolean(bufferedMessages)
cryptOutput.writeBytes(kryoRegDetails)
return iv + aesCipher.doFinal(cryptOutput.toBytes())
@ -217,7 +300,7 @@ internal class CryptoManagement(val logger: KLogger,
// NOTE: ALWAYS CALLED ON THE SAME THREAD! (from the client, mutually exclusive calls to encrypt)
fun decrypt(registrationData: ByteArray, serverPublicKeyBytes: ByteArray): ClientConnectionInfo? {
try {
return try {
val secretKeySpec = generateAesKey(serverPublicKeyBytes, publicKeyBytes, serverPublicKeyBytes)
// now decrypt the data
@ -226,21 +309,11 @@ internal class CryptoManagement(val logger: KLogger,
cryptInput.buffer = aesCipher.doFinal(registrationData, GCM_IV_LENGTH_BYTES, registrationData.size - GCM_IV_LENGTH_BYTES)
val sessionId = cryptInput.readInt()
val streamId = cryptInput.readInt()
val subscriptionPort = cryptInput.readInt()
val regDetailsSize = cryptInput.readInt()
val regDetails = cryptInput.readBytes(regDetailsSize)
makeInfo(serverPublicKeyBytes, secretKeySpec)
// now read data off
return ClientConnectionInfo(sessionId = sessionId,
streamId = streamId,
port = subscriptionPort,
publicKey = serverPublicKeyBytes,
kryoRegistrationDetails = regDetails)
} catch (e: Exception) {
logger.error("Error during AES decrypt!", e)
return null
null
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,13 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.rmi.messages
/**
* @param rmiId which rmi object was deleted
*/
data class ConnectionObjectDeleteResponse(val rmiId: Int) : RmiMessage {
override fun toString(): String {
return "ConnectionObjectDeleteResponse(id: $rmiId)"
package dorkbox.network.connection
class DisconnectMessage(val closeEverything: Boolean) {
companion object {
val CLOSE_SIMPLE = DisconnectMessage(false)
val CLOSE_EVERYTHING = DisconnectMessage(true)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,179 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection
import dorkbox.network.Configuration
import dorkbox.util.NamedThreadFactory
import kotlinx.atomicfu.atomic
import org.slf4j.LoggerFactory
import java.util.concurrent.*
/**
* Event logic throughout the network MUST be run on multiple threads! There are deadlock issues if it is only one, or if the client + server
* share an event dispatcher (multiple network restarts were required to check this)
*
* WARNING: The logic in this class will ONLY work in this class, as it relies on this specific behavior. Do not use it elsewhere!
*/
internal class EventDispatcher(val type: String) {
enum class EDType {
// CLOSE must be last!
HANDSHAKE, CONNECT, ERROR, CLOSE
}
internal class ED(private val dispatcher: EventDispatcher, private val type: EDType) {
fun launch(function: () -> Unit) {
dispatcher.launch(type, function)
}
fun isDispatch(): Boolean {
return dispatcher.isDispatch(type)
}
fun shutdownAndWait(timeout: Long, timeoutUnit: TimeUnit) {
dispatcher.shutdownAndWait(type, timeout, timeoutUnit)
}
}
companion object {
private val DEBUG_EVENTS = false
private val traceId = atomic(0)
private val typedEntries: Array<EDType>
init {
typedEntries = EDType.entries.toTypedArray()
}
}
private val logger = LoggerFactory.getLogger("$type Dispatch")
private val threadIds = EDType.entries.map { atomic(0L) }.toTypedArray()
private val executors = EDType.entries.map { event ->
// It CANNOT be the default dispatch because there will be thread starvation
// NOTE: THIS CANNOT CHANGE!! IT WILL BREAK EVERYTHING IF IT CHANGES!
Executors.newSingleThreadExecutor(
NamedThreadFactory(
namePrefix = "$type-${event.name}",
group = Configuration.networkThreadGroup,
threadPriority = Thread.NORM_PRIORITY,
daemon = true
) { thread ->
// when a new thread is created, assign it to the array
threadIds[event.ordinal].lazySet(thread.id)
}
)
}.toTypedArray()
val HANDSHAKE: ED
val CONNECT: ED
val ERROR: ED
val CLOSE: ED
init {
executors.forEachIndexed { _, executor ->
executor.submit {
// this is to create a new thread only, so that the thread ID can be assigned
}
}
HANDSHAKE = ED(this, EDType.HANDSHAKE)
CONNECT = ED(this, EDType.CONNECT)
ERROR = ED(this, EDType.ERROR)
CLOSE = ED(this, EDType.CLOSE)
}
/**
* Shuts-down each event dispatcher executor, and waits for it to gracefully shutdown. Once shutdown, it cannot be restarted.
*
* @param timeout how long to wait
* @param timeoutUnit what the unit count is
*/
fun shutdownAndWait(timeout: Long, timeoutUnit: TimeUnit) {
require(timeout > 0) { logger.error("The EventDispatcher shutdown timeout must be > 0!") }
HANDSHAKE.shutdownAndWait(timeout, timeoutUnit)
CONNECT.shutdownAndWait(timeout, timeoutUnit)
ERROR.shutdownAndWait(timeout, timeoutUnit)
CLOSE.shutdownAndWait(timeout, timeoutUnit)
}
/**
* Checks if the current execution thread is running inside one of the event dispatchers.
*/
fun isDispatch(): Boolean {
val threadId = Thread.currentThread().id
typedEntries.forEach { event ->
if (threadIds[event.ordinal].value == threadId) {
return true
}
}
return false
}
/**
* Checks if the current execution thread is running inside one of the event dispatchers.
*/
private fun isDispatch(type: EDType): Boolean {
val threadId = Thread.currentThread().id
return threadIds[type.ordinal].value == threadId
}
/**
* shuts-down the current execution thread and waits for it complete.
*/
private fun shutdownAndWait(type: EDType, timeout: Long, timeoutUnit: TimeUnit) {
executors[type.ordinal].shutdown()
executors[type.ordinal].awaitTermination(timeout, timeoutUnit)
}
/**
* Each event type runs inside its own thread executor.
*
* We want EACH event type to run in its own executor... on its OWN thread, in order to prevent deadlocks
* This is because there are blocking dependencies: DISCONNECT -> CONNECT.
*
* If an event is RE-ENTRANT, then it will immediately execute!
*/
private fun launch(event: EDType, function: () -> Unit) {
val eventId = event.ordinal
try {
if (DEBUG_EVENTS) {
val id = traceId.getAndIncrement()
executors[eventId].submit {
if (logger.isDebugEnabled) {
logger.debug("Starting $event : $id")
}
function()
if (logger.isDebugEnabled) {
logger.debug("Finished $event : $id")
}
}
} else {
executors[eventId].submit(function)
}
} catch (e: Exception) {
logger.error("Error during event dispatch!", e)
}
}
}

View File

@ -0,0 +1,231 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection
import dorkbox.netUtil.IPv4
import dorkbox.netUtil.IPv6
import dorkbox.network.ServerConfiguration
import dorkbox.network.connection.IpInfo.Companion.IpListenType.IPC
import dorkbox.network.connection.IpInfo.Companion.IpListenType.IPWildcard
import dorkbox.network.connection.IpInfo.Companion.IpListenType.IPv4Wildcard
import dorkbox.network.connection.IpInfo.Companion.IpListenType.IPv6Wildcard
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
internal class IpInfo(config: ServerConfiguration) {
companion object {
enum class IpListenType {
IPv4, IPv6, IPv4Wildcard, IPv6Wildcard, IPWildcard, IPC
}
fun isLocalhost(ipAddress: String): Boolean {
return when (ipAddress.lowercase()) {
"loopback", "localhost", "lo", "127.0.0.1", "::1" -> true
else -> false
}
}
fun isWildcard(ipAddress: String): Boolean {
return when (ipAddress) {
// this is the "wildcard" address. Windows has problems with this.
"0", "::", "0.0.0.0", "*" -> true
else -> false
}
}
fun isWildcard(ipAddress: InetAddress): Boolean {
return when (ipAddress) {
// this is the "wildcard" address. Windows has problems with this.
IPv4.WILDCARD, IPv6.WILDCARD -> true
else -> false
}
}
fun getWildcard(ipAddress: InetAddress, ipAddressString: String, shouldBeIpv4: Boolean): String {
return if (isWildcard(ipAddress)) {
if (shouldBeIpv4) {
IPv4.WILDCARD_STRING
} else {
IPv6.WILDCARD_STRING
}
} else {
ipAddressString
}
}
fun formatCommonAddress(ipAddress: String, isIpv4: Boolean, elseAction: () -> InetAddress?): InetAddress? {
return if (isLocalhost(ipAddress)) {
if (isIpv4) { IPv4.LOCALHOST } else { IPv6.LOCALHOST }
} else if (isWildcard(ipAddress)) {
if (isIpv4) { IPv4.WILDCARD } else { IPv6.WILDCARD }
} else if (IPv4.isValid(ipAddress)) {
IPv4.toAddress(ipAddress)!!
} else if (IPv6.isValid(ipAddress)) {
IPv6.toAddress(ipAddress)!!
} else {
elseAction()
}
}
fun formatCommonAddressString(ipAddress: String, isIpv4: Boolean, elseAction: () -> String = { ipAddress }): String {
return if (isLocalhost(ipAddress)) {
if (isIpv4) { IPv4.LOCALHOST_STRING } else { IPv6.LOCALHOST_STRING }
} else if (isWildcard(ipAddress)) {
if (isIpv4) { IPv4.WILDCARD_STRING } else { IPv6.WILDCARD_STRING }
} else if (IPv4.isValid(ipAddress)) {
ipAddress
} else if (IPv6.isValid(ipAddress)) {
ipAddress
} else {
elseAction()
}
}
}
val ipType: IpListenType
val listenAddress: InetAddress?
val listenAddressString: String
val formattedListenAddressString: String
val listenAddressStringPretty: String
val isReliable = config.isReliable
val isIpv4: Boolean
init {
val canUseIPv4 = config.enableIPv4 && IPv4.isAvailable
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) we listen in IPv6 WILDCARD
var listenAddress: InetAddress?
var ip46Wildcard = false
when {
canUseIPv4 && canUseIPv6 -> {
// if it's not a valid IP, the lambda will return null
listenAddress = formatCommonAddress(config.listenIpAddress, false) { null }
if (listenAddress == null) {
listenAddress = formatCommonAddress(config.listenIpAddress, true) { null }
} else {
ip46Wildcard = true
}
}
canUseIPv4 -> {
// if it's not a valid IP, the lambda will return null
listenAddress = formatCommonAddress(config.listenIpAddress, true) { null }
}
canUseIPv6 -> {
// if it's not a valid IP, the lambda will return null
listenAddress = formatCommonAddress(config.listenIpAddress, false) { null }
}
else -> {
listenAddress = null
}
}
this.listenAddress = listenAddress
isIpv4 = listenAddress is Inet4Address
// if we are IPv6 WILDCARD -- then our listen-address must ALSO be IPv6, even if our connection is via IPv4
when (listenAddress) {
IPv6.WILDCARD -> {
ipType = if (ip46Wildcard) {
IPWildcard
} else {
IPv6Wildcard
}
listenAddressString = IPv6.WILDCARD_STRING
formattedListenAddressString = if (listenAddressString[0] == '[') {
listenAddressString
} else {
// there MUST be [] surrounding the IPv6 address for aeron to like it!
"[$listenAddressString]"
}
}
IPv4.WILDCARD -> {
ipType = IPv4Wildcard
listenAddressString = IPv4.WILDCARD_STRING
formattedListenAddressString = listenAddressString
}
is Inet6Address -> {
ipType = IpListenType.IPv6
listenAddressString = IPv6.toString(listenAddress)
formattedListenAddressString = if (listenAddressString[0] == '[') {
listenAddressString
} else {
// there MUST be [] surrounding the IPv6 address for aeron to like it!
"[$listenAddressString]"
}
}
is Inet4Address -> {
ipType = IpListenType.IPv4
listenAddressString = IPv4.toString(listenAddress)
formattedListenAddressString = listenAddressString
}
else -> {
ipType = IPC
listenAddressString = EndPoint.IPC_NAME
formattedListenAddressString = listenAddressString
}
}
listenAddressStringPretty = when (listenAddress) {
IPv4.WILDCARD -> listenAddressString
IPv6.WILDCARD -> IPv4.WILDCARD.hostAddress + "/" + listenAddressString
else -> listenAddressString
}
}
/**
* if we are listening on :: (ipv6), and a connection via ipv4 arrives, aeron MUST publish on the IPv4 version
*/
fun getAeronPubAddress(remoteIpv4: Boolean): String {
return if (remoteIpv4) {
when (ipType) {
IPWildcard -> IPv4.WILDCARD_STRING
else -> formattedListenAddressString
}
} else {
formattedListenAddressString
}
}
/**
* if we are listening on :: (ipv6), and a connection via ipv4 arrives, aeron MUST publish on the IPv6 version
*/
fun getAeronSubAddress(remoteIpv4: Boolean): String {
return if (remoteIpv4) {
when (ipType) {
IPWildcard -> IPv4.WILDCARD_STRING
else -> formattedListenAddressString
}
} else {
formattedListenAddressString
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,22 +15,21 @@
*/
package dorkbox.network.connection
import dorkbox.classUtil.ClassHelper
import dorkbox.classUtil.ClassHierarchy
import dorkbox.collections.IdentityMap
import dorkbox.network.ipFilter.IpFilterRule
import dorkbox.os.OS
import dorkbox.util.classes.ClassHelper
import dorkbox.util.classes.ClassHierarchy
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mu.KLogger
import net.jodah.typetools.TypeResolver
import org.slf4j.Logger
import java.net.InetAddress
import java.util.concurrent.locks.*
import kotlin.concurrent.write
/**
* Manages all of the different connect/disconnect/etc listeners
*/
internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogger) {
internal class ListenerManager<CONNECTION: Connection>(private val logger: Logger, val eventDispatch: EventDispatcher) {
companion object {
/**
* Specifies the load-factor for the IdentityMap used to manage keeping track of the number of connections + listeners
@ -42,13 +41,13 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* Neither of these are useful in resolving exception handling from a users perspective, and only clutter the stacktrace.
*/
fun cleanStackTrace(throwable: Throwable, adjustedStartOfStack: Int = 0) {
fun Throwable.cleanStackTrace(adjustedStartOfStack: Int = 0): Throwable {
// we never care about coroutine stacks, so filter then to start with.
val origStackTrace = throwable.stackTrace
val origStackTrace = this.stackTrace
val size = origStackTrace.size
if (size == 0) {
return
return this
}
val stackTrace = origStackTrace.filterNot {
@ -85,15 +84,17 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
if (newEndIndex > 0) {
if (savedFirstStack != null) {
// we want to save the FIRST stack frame also, maybe
throwable.stackTrace = savedFirstStack + stackTrace.copyOfRange(newStartIndex, newEndIndex)
this.stackTrace = savedFirstStack + stackTrace.copyOfRange(newStartIndex, newEndIndex)
} else {
throwable.stackTrace = stackTrace.copyOfRange(newStartIndex, newEndIndex)
this.stackTrace = stackTrace.copyOfRange(newStartIndex, newEndIndex)
}
} else {
// keep just one, since it's a stack frame INSIDE our network library, and we need that!
throwable.stackTrace = stackTrace.copyOfRange(0, 1)
this.stackTrace = stackTrace.copyOfRange(0, 1)
}
return this
}
/**
@ -101,9 +102,9 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* Neither of these are useful in resolving exception handling from a users perspective, and only clutter the stacktrace.
*/
fun cleanStackTraceInternal(throwable: Throwable) {
fun Throwable.cleanStackTraceInternal() {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
val stackTrace = throwable.stackTrace
val stackTrace = this.stackTrace
val size = stackTrace.size
if (size == 0) {
@ -114,7 +115,7 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
val firstDorkboxIndex = stackTrace.indexOfFirst { it.className.startsWith("dorkbox.network.") }
val lastDorkboxIndex = stackTrace.indexOfLast { it.className.startsWith("dorkbox.network.") }
throwable.stackTrace = stackTrace.filterIndexed { index, element ->
this.stackTrace = stackTrace.filterIndexed { index, element ->
val stackName = element.className
if (index <= firstDorkboxIndex && index >= lastDorkboxIndex) {
false
@ -131,65 +132,83 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* We only want the error message, because we do something based on it (and the full stack trace is meaningless)
*/
fun cleanAllStackTrace(throwable: Throwable) {
val stackTrace = throwable.stackTrace
fun Throwable.cleanAllStackTrace(): Throwable{
val stackTrace = this.stackTrace
val size = stackTrace.size
if (size == 0) {
return
return this
}
// throw everything out
throwable.stackTrace = stackTrace.copyOfRange(0, 1)
this.stackTrace = stackTrace.copyOfRange(0, 1)
return this
}
internal inline fun <reified T: Any> add(thing: T, array: Array<T>): Array<T> {
val currentLength: Int = array.size
// add the new subscription to the END of the array
@Suppress("UNCHECKED_CAST")
val newMessageArray = array.copyOf(currentLength + 1) as Array<T>
newMessageArray[currentLength] = thing
return newMessageArray
}
internal inline fun <reified T: Any> remove(thing: T, array: Array<T>): Array<T> {
// remove the subscription form the array
// THIS IS IDENTITY CHECKS, NOT EQUALITY
return array.filter { it !== thing }.toTypedArray()
}
}
// initialize a emtpy arrays
private val onConnectFilterList = atomic(Array<(CONNECTION.() -> Boolean)>(0) { { true } })
private val onConnectFilterMutex = Mutex()
// initialize emtpy arrays
@Volatile
private var onConnectFilterList = Array<((InetAddress, String) -> Boolean)>(0) { { _, _ -> true } }
private val onConnectFilterLock = ReentrantReadWriteLock()
private val onInitList = atomic(Array<suspend (CONNECTION.() -> Unit)>(0) { { } })
private val onInitMutex = Mutex()
@Volatile
private var onConnectBufferedMessageFilterList = Array<((InetAddress?, String) -> Boolean)>(0) { { _, _ -> true } }
private val onConnectBufferedMessageFilterLock = ReentrantReadWriteLock()
private val onConnectList = atomic(Array<suspend (CONNECTION.() -> Unit)>(0) { { } })
private val onConnectMutex = Mutex()
@Volatile
private var onInitList = Array<(CONNECTION.() -> Unit)>(0) { { } }
private val onInitLock = ReentrantReadWriteLock()
private val onDisconnectList = atomic(Array<suspend CONNECTION.() -> Unit>(0) { { } })
private val onDisconnectMutex = Mutex()
@Volatile
private var onConnectList = Array<(CONNECTION.() -> Unit)>(0) { { } }
private val onConnectLock = ReentrantReadWriteLock()
private val onErrorList = atomic(Array<CONNECTION.(Throwable) -> Unit>(0) { { } })
private val onErrorMutex = Mutex()
@Volatile
private var onDisconnectList = Array<CONNECTION.() -> Unit>(0) { { } }
private val onDisconnectLock = ReentrantReadWriteLock()
private val onErrorGlobalList = atomic(Array<Throwable.() -> Unit>(0) { { } })
private val onErrorGlobalMutex = Mutex()
@Volatile
private var onErrorList = Array<CONNECTION.(Throwable) -> Unit>(0) { { } }
private val onErrorLock = ReentrantReadWriteLock()
private val onMessageMap = atomic(IdentityMap<Class<*>, Array<suspend CONNECTION.(Any) -> Unit>>(32, LOAD_FACTOR))
private val onMessageMutex = Mutex()
@Volatile
private var onErrorGlobalList = Array<Throwable.() -> Unit>(0) { { } }
private val onErrorGlobalLock = ReentrantReadWriteLock()
@Volatile
private var onMessageMap = IdentityMap<Class<*>, Array<CONNECTION.(Any) -> Unit>>(32, LOAD_FACTOR)
private val onMessageLock = ReentrantReadWriteLock()
// used to keep a cache of class hierarchy for distributing messages
private val classHierarchyCache = ClassHierarchy(LOAD_FACTOR)
private inline fun <reified T> add(thing: T, array: Array<T>): Array<T> {
val currentLength: Int = array.size
// add the new subscription to the array
@Suppress("UNCHECKED_CAST")
val newMessageArray = array.copyOf(currentLength + 1) as Array<T>
newMessageArray[currentLength] = thing
return newMessageArray
}
/**
* Adds an IP+subnet rule that defines if that IP+subnet is allowed or denied connectivity to this server.
*
* If there are no rules added, then all connections are allowed
* If there are rules added, then a rule MUST be matched to be allowed
*/
suspend fun filter(ipFilterRule: IpFilterRule) {
filter {
// IPC will not filter, so this is OK to coerce to not-null
ipFilterRule.matches(remoteAddress!!)
fun filter(ipFilterRule: IpFilterRule) {
filter { clientAddress, _ ->
// IPC will not filter
ipFilterRule.matches(clientAddress)
}
}
@ -198,18 +217,51 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
* Adds a function that will be called BEFORE a client/server "connects" with each other, and used to determine if a connection
* should be allowed
*
* By default, if there are no filter rules, then all connections are allowed to connect
* If there are filter rules - then ONLY connections for the filter that returns true are allowed to connect (all else are denied)
*
* It is the responsibility of the custom filter to write the error, if there is one
*
* If the function returns TRUE, then the connection will continue to connect.
* If the function returns FALSE, then the other end of the connection will
* receive a connection error
*
* For a server, this function will be called for ALL clients.
*
* If ANY filter rule that is applied returns true, then the connection is permitted
*
* This function will be called for **only** network clients (IPC client are excluded)
*
* @param function clientAddress: UDP connection address
* tagName: the connection tag name
*/
suspend fun filter(function: CONNECTION.() -> Boolean) {
onConnectFilterMutex.withLock {
fun filter(function: (clientAddress: InetAddress, tagName: String) -> Boolean) {
onConnectFilterLock.write {
// we have to follow the single-writer principle!
onConnectFilterList.lazySet(add(function, onConnectFilterList.value))
onConnectFilterList = add(function, onConnectFilterList)
}
}
/**
* Adds a function that will be called BEFORE a client/server "connects" with each other, and used to determine if buffered messages
* for a connection should be enabled
*
* By default, if there are no rules, then all connections will have buffered messages enabled
* If there are rules - then ONLY connections for the rule that returns true will have buffered messages enabled (all else are disabled)
*
* It is the responsibility of the custom filter to write the error, if there is one
*
* If the function returns TRUE, then the buffered messages for a connection are enabled.
* If the function returns FALSE, then the buffered messages for a connection is disabled.
*
* If ANY rule that is applied returns true, then the buffered messages for a connection are enabled
*
* @param function clientAddress: not-null when UDP connection, null when IPC connection
* tagName: the connection tag name
*/
fun enableBufferedMessages(function: (clientAddress: InetAddress?, tagName: String) -> Boolean) {
onConnectBufferedMessageFilterLock.write {
// we have to follow the single-writer principle!
onConnectBufferedMessageFilterList = add(function, onConnectBufferedMessageFilterList)
}
}
@ -219,10 +271,10 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* For a server, this function will be called for ALL client connections.
*/
suspend fun onInit(function: suspend CONNECTION.() -> Unit) {
onInitMutex.withLock {
fun onInit(function: CONNECTION.() -> Unit) {
onInitLock.write {
// we have to follow the single-writer principle!
onInitList.lazySet(add(function, onInitList.value))
onInitList = add(function, onInitList)
}
}
@ -230,10 +282,10 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
* Adds a function that will be called when a client/server connection first establishes a connection with the remote end.
* 'onInit()' callbacks will execute for both the client and server before `onConnect()` will execute will "connects" with each other
*/
suspend fun onConnect(function: suspend CONNECTION.() -> Unit) {
onConnectMutex.withLock {
fun onConnect(function: CONNECTION.() -> Unit) {
onConnectLock.write {
// we have to follow the single-writer principle!
onConnectList.lazySet(add(function, onConnectList.value))
onConnectList = add(function, onConnectList)
}
}
@ -242,10 +294,10 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* Do not try to send messages! The connection will already be closed, resulting in an error if you attempt to do so.
*/
suspend fun onDisconnect(function: suspend CONNECTION.() -> Unit) {
onDisconnectMutex.withLock {
fun onDisconnect(function: CONNECTION.() -> Unit) {
onDisconnectLock.write {
// we have to follow the single-writer principle!
onDisconnectList.lazySet(add(function, onDisconnectList.value))
onDisconnectList = add(function, onDisconnectList)
}
}
@ -254,10 +306,10 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* The error is also sent to an error log before this method is called.
*/
suspend fun onError(function: CONNECTION.(Throwable) -> Unit) {
onErrorMutex.withLock {
fun onError(function: CONNECTION.(Throwable) -> Unit) {
onErrorLock.write {
// we have to follow the single-writer principle!
onErrorList.lazySet(add(function, onErrorList.value))
onErrorList = add(function, onErrorList)
}
}
@ -266,10 +318,10 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* The error is also sent to an error log before this method is called.
*/
suspend fun onError(function: Throwable.() -> Unit) {
onErrorGlobalMutex.withLock {
fun onError(function: Throwable.() -> Unit) {
onErrorGlobalLock.write {
// we have to follow the single-writer principle!
onErrorGlobalList.lazySet(add(function, onErrorGlobalList.value))
onErrorGlobalList = add(function, onErrorGlobalList)
}
}
@ -278,8 +330,8 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* This method should not block for long periods as other network activity will not be processed until it returns.
*/
suspend fun <MESSAGE> onMessage(function: suspend CONNECTION.(MESSAGE) -> Unit) {
onMessageMutex.withLock {
fun <MESSAGE> onMessage(function: CONNECTION.(MESSAGE) -> Unit) {
onMessageLock.write {
// we have to follow the single-writer principle!
// this is the connection generic parameter for the listener, works for lambda expressions as well
@ -300,27 +352,27 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
}
if (success) {
// NOTE: https://github.com/Kotlin/kotlinx.atomicfu
// https://github.com/Kotlin/kotlinx.atomicfu
// this is EXPLICITLY listed as a "Don't" via the documentation. The ****ONLY**** reason this is actually OK is because
// we are following the "single-writer principle", so only ONE THREAD can modify this at a time.
val tempMap = onMessageMap.value
val tempMap = onMessageMap
@Suppress("UNCHECKED_CAST")
val func = function as suspend (CONNECTION, Any) -> Unit
val func = function as (CONNECTION, Any) -> Unit
val newMessageArray: Array<suspend (CONNECTION, Any) -> Unit>
val onMessageArray: Array<suspend (CONNECTION, Any) -> Unit>? = tempMap.get(messageClass)
val newMessageArray: Array<(CONNECTION, Any) -> Unit>
val onMessageArray: Array<(CONNECTION, Any) -> Unit>? = tempMap[messageClass]
if (onMessageArray != null) {
newMessageArray = add(function, onMessageArray)
} else {
@Suppress("RemoveExplicitTypeArguments")
newMessageArray = Array<suspend (CONNECTION, Any) -> Unit>(1) { { _, _ -> } }
newMessageArray = Array<(CONNECTION, Any) -> Unit>(1) { { _, _ -> } }
newMessageArray[0] = func
}
tempMap.put(messageClass, newMessageArray)
onMessageMap.lazySet(tempMap)
tempMap.put(messageClass!!, newMessageArray)
onMessageMap = tempMap
} else {
throw IllegalArgumentException("Unable to add incompatible types! Detected connection/message classes: $connectionClass, $messageClass")
}
@ -332,23 +384,18 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* It is the responsibility of the custom filter to write the error, if there is one
*
* @return true if the connection will be allowed to connect. False if we should terminate this connection
* This is run directly on the thread that calls it!
*
* @return true if the client address is allowed to connect. False if we should terminate this connection
*/
fun notifyFilter(connection: CONNECTION): Boolean {
// remote address will NOT be null at this stage, but best to verify.
val remoteAddress = connection.remoteAddress
if (remoteAddress == null) {
logger.error("Connection ${connection.id}: Unable to attempt connection stages when no remote address is present")
return false
}
fun notifyFilter(clientAddress: InetAddress, clientTagName: String): Boolean {
// by default, there is a SINGLE rule that will always exist, and will always ACCEPT ALL connections.
// This is so the array types can be setup (the compiler needs SOMETHING there)
val arrayOfIpFilterRules = onConnectFilterList.value
val list = onConnectFilterList
// if there is a rule, a connection must match for it to connect
arrayOfIpFilterRules.forEach {
if (it.invoke(connection)) {
list.forEach {
if (it.invoke(clientAddress, clientTagName)) {
return true
}
}
@ -356,70 +403,135 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
// default if nothing matches
// NO RULES ADDED -> ACCEPT
// RULES ADDED -> DENY
return arrayOfIpFilterRules.isEmpty()
return list.isEmpty()
}
/**
* Invoked just after a connection is created, but before it is connected.
*
* It is the responsibility of the custom filter to write the error, if there is one
*
* This is run directly on the thread that calls it!
*
* @return true if the connection will have buffered messages enabled. False if buffered messages for this connection should be disabled.
*/
fun notifyEnableBufferedMessages(clientAddress: InetAddress?, clientTagName: String): Boolean {
// by default, there is a SINGLE rule that will always exist, and will always PERMIT buffered messages.
// This is so the array types can be setup (the compiler needs SOMETHING there)
val list = onConnectBufferedMessageFilterList
// if there is a rule, a connection must match for it to enable buffered messages
list.forEach {
if (it.invoke(clientAddress, clientTagName)) {
return true
}
}
// default if nothing matches
// NO RULES ADDED -> ALLOW Buffered Messages
// RULES ADDED -> DISABLE Buffered Messages
return list.isEmpty()
}
/**
* Invoked when a connection is first initialized, but BEFORE it's connected to the remote address.
*
* NOTE: This is run directly on the thread that calls it! Things that happen in event are TIME-CRITICAL, and must happen before connect happens.
* Because of this guarantee, init is immediately executed where connect is on a separate thread
*/
fun notifyInit(connection: CONNECTION) {
runBlocking {
onInitList.value.forEach {
try {
it(connection)
} catch (t: Throwable) {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
cleanStackTrace(t)
logger.error("Connection ${connection.id} error", t)
}
val list = onInitList
list.forEach {
try {
it(connection)
} catch (t: Throwable) {
// when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
t.cleanStackTrace()
logger.error("Connection ${connection.id} error", t)
}
}
}
/**
* Invoked when a connection is connected to a remote address.
*
* This is run on the EventDispatch!
*/
suspend fun notifyConnect(connection: CONNECTION) {
onConnectList.value.forEach {
try {
it(connection)
} catch (t: Throwable) {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
cleanStackTrace(t)
logger.error("Connection ${connection.id} error", t)
fun notifyConnect(connection: CONNECTION) {
val list = onConnectList
if (list.isNotEmpty()) {
connection.endPoint.eventDispatch.CONNECT.launch {
list.forEach {
try {
it(connection)
} catch (t: Throwable) {
// when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
t.cleanStackTrace()
logger.error("Connection ${connection.id} error", t)
}
}
}
}
}
/**
* Invoked when a connection is disconnected to a remote address.
*
* This is exclusively called from a connection, when that connection is closed!
*
* This is run on the EventDispatch!
*/
suspend fun notifyDisconnect(connection: CONNECTION) {
onDisconnectList.value.forEach {
try {
it(connection)
} catch (t: Throwable) {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
cleanStackTrace(t)
logger.error("Connection ${connection.id} error", t)
fun notifyDisconnect(connection: Connection) {
connection.notifyDisconnect()
@Suppress("UNCHECKED_CAST")
directNotifyDisconnect(connection as CONNECTION)
}
/**
* This is invoked by either a GLOBAL listener manager, or for a SPECIFIC CONNECTION listener manager.
*/
fun directNotifyDisconnect(connection: CONNECTION) {
val list = onDisconnectList
if (list.isNotEmpty()) {
connection.endPoint.eventDispatch.CLOSE.launch {
list.forEach {
try {
it(connection)
} catch (t: Throwable) {
// when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
t.cleanStackTrace()
logger.error("Connection ${connection.id} error", t)
}
}
}
}
}
/**
* Invoked when there is an error for a specific connection
*
* The error is also sent to an error log before notifying callbacks
*
* This is run on the EventDispatch!
*/
fun notifyError(connection: CONNECTION, exception: Throwable) {
onErrorList.value.forEach {
try {
it(connection, exception)
} catch (t: Throwable) {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
cleanStackTrace(t)
logger.error("Connection ${connection.id} error", t)
val list = onErrorList
if (list.isNotEmpty()) {
connection.endPoint.eventDispatch.ERROR.launch {
list.forEach {
try {
it(connection, exception)
} catch (t: Throwable) {
// when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
t.cleanStackTrace()
logger.error("Connection ${connection.id} error", t)
}
}
}
} else {
logger.error("Error with connection $connection", exception)
}
}
@ -428,15 +540,22 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* The error is also sent to an error log before notifying callbacks
*/
val notifyError: (exception: Throwable) -> Unit = { exception ->
onErrorGlobalList.value.forEach {
try {
it(exception)
} catch (t: Throwable) {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
cleanStackTrace(t)
logger.error("Global error", t)
fun notifyError(exception: Throwable) {
val list = onErrorGlobalList
if (list.isNotEmpty()) {
eventDispatch.ERROR.launch {
list.forEach {
try {
it(exception)
} catch (t: Throwable) {
// when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
t.cleanStackTrace()
logger.error("Global error", t)
}
}
}
} else {
logger.error("Global error", exception)
}
}
@ -445,7 +564,7 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
*
* @return true if there were listeners assigned for this message type
*/
suspend fun notifyOnMessage(connection: CONNECTION, message: Any): Boolean {
fun notifyOnMessage(connection: CONNECTION, message: Any): Boolean {
val messageClass: Class<*> = message.javaClass
// have to save the types + hierarchy (note: duplicates are OK, since they will just be overwritten)
@ -465,10 +584,10 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
// cache the lookup
// we don't care about race conditions, since the object hierarchy will be ALREADY established at this exact moment
val tempMap = onMessageMap.value
val tempMap = onMessageMap
var hasListeners = false
hierarchy.forEach { clazz ->
val onMessageArray: Array<suspend (CONNECTION, Any) -> Unit>? = tempMap.get(clazz)
val onMessageArray: Array<(CONNECTION, Any) -> Unit>? = tempMap[clazz]
if (onMessageArray != null) {
hasListeners = true
@ -476,8 +595,6 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
try {
func(connection, message)
} catch (t: Throwable) {
cleanStackTrace(t)
logger.error("Connection ${connection.id} error", t)
notifyError(connection, t)
}
}
@ -486,4 +603,37 @@ internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogg
return hasListeners
}
/**
* This will remove all listeners that have been registered!
*/
fun close() {
// we have to follow the single-writer principle!
logger.debug("Closing the listener manager")
onConnectFilterLock.write {
onConnectFilterList = Array(0) { { _, _ -> true } }
}
onConnectBufferedMessageFilterLock.write {
onConnectBufferedMessageFilterList = Array(0) { { _, _ -> true } }
}
onInitLock.write {
onInitList = Array(0) { { } }
}
onConnectLock.write {
onConnectList = Array(0) { { } }
}
onDisconnectLock.write {
onDisconnectList = Array(0) { { } }
}
onErrorLock.write {
onErrorList = Array(0) { { } }
}
onErrorGlobalLock.write {
onErrorGlobalList = Array(0) { { } }
}
onMessageLock.write {
onMessageMap = IdentityMap(32, LOAD_FACTOR)
}
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection
class Paired<CONNECTION : Connection> {
lateinit var connection: CONNECTION
lateinit var message: Any
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection
import dorkbox.network.rmi.RmiUtils
class SendSync {
var message: Any? = null
// used to notify the remote endpoint that the message has been processed
var id: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SendSync) return false
if (message != other.message) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = message?.hashCode() ?: 0
result = 31 * result + id
return result
}
override fun toString(): String {
return "SendSync ${RmiUtils.unpackUnsignedRight(id)} (message=$message)"
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.buffer
import dorkbox.bytes.ByteArrayWrapper
import dorkbox.collections.LockFreeHashMap
import dorkbox.hex.toHexString
import dorkbox.network.Configuration
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.connection.Connection
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.ListenerManager
import dorkbox.util.Sys
import net.jodah.expiringmap.ExpirationPolicy
import net.jodah.expiringmap.ExpiringMap
import org.slf4j.LoggerFactory
import java.util.concurrent.*
internal open class BufferManager<CONNECTION: Connection>(
config: Configuration,
listenerManager: ListenerManager<CONNECTION>,
aeronDriver: AeronDriver,
sessionTimeout: Long
) {
companion object {
private val logger = LoggerFactory.getLogger(BufferManager::class.java.simpleName)
}
private val sessions = LockFreeHashMap<ByteArrayWrapper, BufferedSession>()
private val expiringSessions: ExpiringMap<ByteArrayWrapper, BufferedSession>
init {
require(sessionTimeout >= 60) { "The buffered connection timeout 'bufferedConnectionTimeoutSeconds' must be greater than 60 seconds!" }
// ignore 0
val check = TimeUnit.SECONDS.toNanos(sessionTimeout)
val lingerNs = aeronDriver.lingerNs()
val required = TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong())
require(check == 0L || check > required + lingerNs) {
"The session timeout (${Sys.getTimePretty(check)}) must be longer than the connection close timeout (${Sys.getTimePretty(required)}) + the aeron driver linger timeout (${Sys.getTimePretty(lingerNs)})!"
}
// connections are extremely difficult to diagnose when the connection timeout is short
val timeUnit = if (EndPoint.DEBUG_CONNECTIONS) { TimeUnit.HOURS } else { TimeUnit.SECONDS }
expiringSessions = ExpiringMap.builder()
.expiration(sessionTimeout, timeUnit)
.expirationPolicy(ExpirationPolicy.CREATED)
.expirationListener<ByteArrayWrapper, BufferedSession> { publicKeyWrapped, sessionConnection ->
// this blocks until it fully runs (which is ok. this is fast)
logger.debug("Connection session expired for: ${publicKeyWrapped.bytes.toHexString()}")
// this SESSION has expired, so we should call the onDisconnect for the underlying connection, in order to clean it up.
listenerManager.notifyDisconnect(sessionConnection.connection)
}
.build()
}
/**
* this must be called when a new connection is created
*
* @return true if this is a new session, false if it is an existing session
*/
fun onConnect(connection: Connection): BufferedSession {
val publicKeyWrapped = ByteArrayWrapper.wrap(connection.uuid)
return synchronized(sessions) {
// always check if we are expiring first...
val expiring = expiringSessions.remove(publicKeyWrapped)
if (expiring != null) {
expiring.connection = connection
expiring
} else {
val existing = sessions[publicKeyWrapped]
if (existing != null) {
// we must always set this session value!!
existing.connection = connection
existing
} else {
val newSession = BufferedSession(connection)
sessions[publicKeyWrapped] = newSession
// we must always set this when the connection is created, and it must be inside the sync block!
newSession
}
}
}
}
/**
* Always called when a connection is disconnected from the network
*/
fun onDisconnect(connection: Connection) {
try {
val publicKeyWrapped = ByteArrayWrapper.wrap(connection.uuid)
synchronized(sessions) {
val sess = sessions.remove(publicKeyWrapped)
// we want to expire this session after XYZ time
expiringSessions[publicKeyWrapped] = sess
}
}
catch (e: Exception) {
logger.error("Unable to run session expire logic!", e)
}
}
fun close() {
synchronized(sessions) {
sessions.clear()
expiringSessions.clear()
}
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.buffer
class BufferedMessages {
var messages = arrayListOf<Any>()
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.buffer
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
internal class BufferedSerializer: Serializer<BufferedMessages>() {
override fun write(kryo: Kryo, output: Output, messages: BufferedMessages) {
kryo.writeClassAndObject(output, messages.messages)
}
override fun read(kryo: Kryo, input: Input, type: Class<out BufferedMessages>): BufferedMessages {
val messages = BufferedMessages()
messages.messages = kryo.readClassAndObject(input) as ArrayList<Any>
return messages
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.buffer
import dorkbox.network.connection.Connection
import java.util.concurrent.*
open class BufferedSession(@Volatile var connection: Connection) {
/**
* Only used when configured. Will re-send all missing messages to a connection when a connection re-connects.
*/
val pendingMessagesQueue: LinkedTransferQueue<Any> = LinkedTransferQueue()
fun queueMessage(connection: Connection, message: Any, abortEarly: Boolean): Boolean {
if (this.connection != connection) {
connection.logger.trace("[{}] message received on old connection, resending", connection)
// we received a message on an OLD connection (which is no longer connected ---- BUT we have a NEW connection that is connected)
// this can happen on RMI object that are old
val success = this.connection.send(message, abortEarly)
if (success) {
connection.logger.trace("[{}] successfully resent message", connection)
return true
}
}
if (!connection.enableBufferedMessages) {
// nothing, since we emit logs during connection initialization that pending messages are DISABLED
return false
}
if (!abortEarly) {
// this was a "normal" send (instead of the disconnect message).
pendingMessagesQueue.put(message)
connection.logger.trace("[{}] queueing message", connection)
}
else if (connection.endPoint.aeronDriver.internal.mustRestartDriverOnError) {
// the only way we get errors, is if the connection is bad OR if we are sending so fast that the connection cannot keep up.
// don't restart/reconnect -- there was an internal network error
pendingMessagesQueue.put(message)
connection.logger.trace("[{}] queueing message", connection)
}
else if (!connection.isClosedWithTimeout()) {
// there was an issue - the connection should automatically reconnect
pendingMessagesQueue.put(message)
connection.logger.trace("[{}] queueing message", connection)
}
return false
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.buffer;

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection;

View File

@ -0,0 +1,34 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.streaming
import dorkbox.network.serialization.AeronOutput
import kotlinx.atomicfu.atomic
class AeronWriter(val size: Int): StreamingWriter, AeronOutput(size) {
private val written = atomic(0)
override fun writeBytes(startPosition: Int, bytes: ByteArray) {
position = startPosition
writeBytes(bytes)
written.getAndAdd(bytes.size)
}
override fun isFinished(): Boolean {
return written.value == size
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.streaming
import kotlinx.atomicfu.atomic
import java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile
class FileWriter(val size: Int, val file: File) : StreamingWriter, RandomAccessFile(file, "rw") {
private val written = atomic(0)
init {
// reserve space on disk!
val saveSize = size.coerceAtMost(4096)
var bytes = ByteArray(saveSize)
this.write(bytes)
if (saveSize < size) {
var remainingBytes = size - saveSize
while (remainingBytes > 0) {
if (saveSize > remainingBytes) {
bytes = ByteArray(remainingBytes)
}
this.write(bytes)
remainingBytes = (remainingBytes - saveSize).coerceAtLeast(0)
}
}
}
override fun writeBytes(startPosition: Int, bytes: ByteArray) {
// the OS will synchronize writes to disk
this.seek(startPosition.toLong())
write(bytes)
written.addAndGet(bytes.size)
}
override fun isFinished(): Boolean {
return written.value == size
}
fun finishAndClose() {
fd.sync()
close()
}
}

View File

@ -1,5 +1,23 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.streaming
data class StreamingControl(val state: StreamingState, val streamId: Long,
val totalSize: Long = 0L,
val isFile: Boolean = false, val fileName: String = ""): StreamingMessage
data class StreamingControl(val state: StreamingState,
val isFile: Boolean,
val streamId: Int,
val totalSize: Long = 0L
): StreamingMessage

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.streaming
import com.esotericsoftware.kryo.Kryo
@ -20,41 +21,21 @@ import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
class StreamingControlSerializer: Serializer<StreamingControl>() {
internal class StreamingControlSerializer: Serializer<StreamingControl>() {
override fun write(kryo: Kryo, output: Output, data: StreamingControl) {
output.writeByte(data.state.ordinal)
output.writeVarLong(data.streamId, true)
output.writeVarLong(data.totalSize, true)
output.writeBoolean(data.isFile)
if (data.isFile) {
output.writeString(data.fileName)
}
output.writeVarInt(data.streamId, true)
output.writeVarLong(data.totalSize, true)
}
override fun read(kryo: Kryo, input: Input, type: Class<out StreamingControl>): StreamingControl {
val stateOrdinal = input.readByte().toInt()
val state = StreamingState.values().first { it.ordinal == stateOrdinal }
val streamId = input.readVarLong(true)
val totalSize = input.readVarLong(true)
val isFile = input.readBoolean()
val fileName = if (isFile) {
input.readString()
} else {
""
}
val state = StreamingState.entries.first { it.ordinal == stateOrdinal }
val streamId = input.readVarInt(true)
val totalSize = input.readVarLong(true)
return StreamingControl(state, streamId, totalSize, isFile, fileName)
}
}
class StreamingDataSerializer: Serializer<StreamingData>() {
override fun write(kryo: Kryo, output: Output, data: StreamingData) {
output.writeVarLong(data.streamId, true)
}
override fun read(kryo: Kryo, input: Input, type: Class<out StreamingData>): StreamingData {
val streamId = input.readVarLong(true)
return StreamingData(streamId)
return StreamingControl(state, isFile, streamId, totalSize)
}
}

View File

@ -1,9 +1,27 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.streaming
class StreamingData(val streamId: Long) : StreamingMessage {
import dorkbox.bytes.xxHash32
// These are set just after we receive the message, and before we process it
@Transient var payload: ByteArray? = null
class StreamingData(val streamId: Int) : StreamingMessage {
var payload: ByteArray? = null
var startPosition: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -17,16 +35,19 @@ class StreamingData(val streamId: Long) : StreamingMessage {
if (!payload.contentEquals(other.payload)) return false
} else if (other.payload != null) return false
if (startPosition != other.startPosition) return false
return true
}
override fun hashCode(): Int {
var result = streamId.hashCode()
result = 31 * result + (payload?.contentHashCode() ?: 0)
result = 31 * result + (startPosition)
return result
}
override fun toString(): String {
return "StreamingData(streamId=$streamId)"
return "StreamingData(streamId=$streamId position=${startPosition}, xxHash=${payload?.xxHash32()})"
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.streaming
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
internal class StreamingDataSerializer: Serializer<StreamingData>() {
override fun write(kryo: Kryo, output: Output, data: StreamingData) {
output.writeVarInt(data.streamId, true)
// we re-use this data when streaming data to the remote endpoint, so we don't write out the payload here, we do it in another place
}
override fun read(kryo: Kryo, input: Input, type: Class<out StreamingData>): StreamingData {
val streamId = input.readVarInt(true)
val streamingData = StreamingData(streamId)
// we want to read out the start-position AND payload. It is not written by the serializer, but by the streaming manager
val startPosition = input.readVarInt(true)
val payloadSize = input.readVarInt(true)
streamingData.startPosition = startPosition
streamingData.payload = input.readBytes(payloadSize)
return streamingData
}
}

View File

@ -1,30 +1,55 @@
/*
* Copyright 2023 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.
*/
@file:Suppress("DuplicatedCode")
package dorkbox.network.connection.streaming
import com.esotericsoftware.kryo.io.Input
import dorkbox.bytes.OptimizeUtilsByteArray
import dorkbox.bytes.OptimizeUtilsByteBuf
import dorkbox.collections.LockFreeHashMap
import dorkbox.collections.LockFreeLongMap
import dorkbox.network.Configuration
import dorkbox.network.connection.Connection
import dorkbox.network.connection.CryptoManagement
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.ListenerManager
import dorkbox.network.connection.ListenerManager.Companion.cleanAllStackTrace
import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace
import dorkbox.network.serialization.AeronInput
import dorkbox.network.serialization.AeronOutput
import dorkbox.network.serialization.KryoExtra
import dorkbox.network.serialization.KryoWriter
import dorkbox.os.OS
import dorkbox.util.Sys
import io.aeron.Publication
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mu.KLogger
import org.agrona.MutableDirectBuffer
import java.security.SecureRandom
import org.agrona.concurrent.IdleStrategy
import org.agrona.concurrent.UnsafeBuffer
import org.slf4j.Logger
import java.io.File
import java.io.FileInputStream
internal class StreamingManager<CONNECTION : Connection>(private val logger: KLogger, private val actionDispatch: CoroutineScope) {
private val streamingDataTarget = LockFreeHashMap<Long, StreamingControl>()
private val streamingDataInMemory = LockFreeHashMap<Long, AeronOutput>()
internal class StreamingManager<CONNECTION : Connection>(private val logger: Logger, val config: Configuration) {
companion object {
val random = SecureRandom()
private const val KILOBYTE = 1024
private const val MEGABYTE = 1024 * KILOBYTE
private const val GIGABYTE = 1024 * MEGABYTE
private const val TERABYTE = 1024L * GIGABYTE
@Suppress("UNUSED_CHANGED_VALUE")
@Suppress("UNUSED_CHANGED_VALUE", "SameParameterValue")
private fun writeVarInt(internalBuffer: MutableDirectBuffer, position: Int, value: Int, optimizePositive: Boolean): Int {
var p = position
var newValue = value
@ -64,85 +89,171 @@ internal class StreamingManager<CONNECTION : Connection>(private val logger: KLo
}
private val streamingDataTarget = LockFreeLongMap<StreamingControl>()
private val streamingDataInMemory = LockFreeLongMap<StreamingWriter>()
/**
* Reassemble/figure out the internal message pieces
* What is the max stream size that can exist in memory when deciding if data blocks are in memory or temp-file on disk
*/
private val maxStreamSizeInMemoryInBytes = config.maxStreamSizeInMemoryMB * MEGABYTE
fun getFile(connection: CONNECTION, endPoint: EndPoint<CONNECTION>, messageStreamId: Int): File {
// NOTE: the stream session ID is a combination of the connection ID + random ID (on the receiving side),
// otherwise clients can abuse it and corrupt OTHER clients data!!
val streamId = (connection.id.toLong() shl 4) or messageStreamId.toLong()
val output = streamingDataInMemory[streamId]
return if (output is FileWriter) {
streamingDataInMemory.remove(streamId)
output.file
} else {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "Error while reading file output, stream $streamId was of the wrong type!"
// either client or server. No other choices. We create an exception, because it's more useful!
throw endPoint.newException(errorMessage)
}
}
/**
* NOTE: MUST BE ON THE AERON THREAD!
*
* Reassemble/figure out the internal message pieces. Processed always on the same thread
*/
fun processControlMessage(message: StreamingControl, endPoint: EndPoint<CONNECTION>, connection: CONNECTION) {
val streamId = message.streamId
fun processControlMessage(
message: StreamingControl,
endPoint: EndPoint<CONNECTION>,
connection: CONNECTION
) {
// NOTE: the stream session ID is a combination of the connection ID + random ID (on the receiving side),
// otherwise clients can abuse it and corrupt OTHER clients data!!
val streamId = (connection.id.toLong() shl 4) or message.streamId.toLong()
when (message.state) {
StreamingState.START -> {
streamingDataTarget[streamId] = message
if (!message.isFile) {
streamingDataInMemory[streamId] = AeronOutput()
// message.totalSize > maxInMemory OR if we are a file, then write to a temp file INSTEAD
if (message.isFile || message.totalSize > maxStreamSizeInMemoryInBytes) {
var fileName = "${config.appId}_${streamId}_${connection.id}.tmp"
var tempFileLocation = OS.TEMP_DIR.resolve(fileName)
while (tempFileLocation.canRead()) {
fileName = "${config.appId}_${streamId}_${connection.id}_${CryptoManagement.secureRandom.nextInt()}.tmp"
tempFileLocation = OS.TEMP_DIR.resolve(fileName)
}
tempFileLocation.deleteOnExit()
val prettySize = Sys.getSizePretty(message.totalSize)
if (endPoint.logger.isInfoEnabled) {
endPoint.logger.info("Saving $prettySize of streaming data [${streamId}] to: $tempFileLocation")
}
streamingDataInMemory[streamId] = FileWriter(message.totalSize.toInt(), tempFileLocation)
} else {
if (endPoint.logger.isTraceEnabled) {
endPoint.logger.trace("Saving streaming data [${streamId}] in memory")
}
// .toInt is safe because we know the total size is < than maxStreamSizeInMemoryInBytes
streamingDataInMemory[streamId] = AeronWriter(message.totalSize.toInt())
}
// this must be last
streamingDataTarget[streamId] = message
}
StreamingState.FINISHED -> {
// get the data out and send messages!
if (!message.isFile) {
val output = streamingDataInMemory.remove(streamId)
if (output != null) {
val kryo: KryoExtra<CONNECTION> = endPoint.serialization.takeKryo()
// NOTE: cannot be on a coroutine before kryo usage!
try {
val input = AeronInput(output.internalBuffer)
val streamedMessage = kryo.read(input)
if (message.isFile) {
// we do not do anything with this file yet! The serializer has to return this instance!
val output = streamingDataInMemory[streamId]
// NOTE: This MUST be on a new co-routine
actionDispatch.launch {
val listenerManager = endPoint.listenerManager
try {
@Suppress("UNCHECKED_CAST")
var hasListeners = listenerManager.notifyOnMessage(connection, streamedMessage)
// each connection registers, and is polled INDEPENDENTLY for messages.
hasListeners = hasListeners or connection.notifyOnMessage(streamedMessage)
if (!hasListeners) {
logger.error("No message callbacks found for ${streamedMessage::class.java.name}")
}
} catch (e: Exception) {
logger.error("Error processing message ${streamedMessage::class.java.name}", e)
listenerManager.notifyError(connection, e)
}
}
} catch (e: Exception) {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "Error serializing message from received streaming content, stream $streamId"
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage, e)
// +2 because we do not want to see the stack for the abstract `newException`
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 2)
throw exception
} finally {
endPoint.serialization.returnKryo(kryo)
}
if (output is FileWriter) {
output.finishAndClose()
// we don't need to do anything else (no de-serialization into an object) because we are already our target object
return
} else {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "Error while receiving streaming content, stream $streamId not available."
val errorMessage = "Error while processing streaming content, stream $streamId was supposed to be a FileWriter."
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +2 because we do not want to see the stack for the abstract `newException`
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 2)
throw exception
throw endPoint.newException(errorMessage)
}
} else {
// we are a file, so process accordingly
}
// get the data out and send messages!
val output = streamingDataInMemory.remove(streamId)
val input = when (output) {
is AeronWriter -> {
// the position can be wrong, especially if there are multiple threads setting the data
output.setPosition(output.size)
AeronInput(output.internalBuffer)
}
is FileWriter -> {
// if we are too large to fit in memory while streaming, we store it on disk.
output.finishAndClose()
val fileInputStream = FileInputStream(output.file)
Input(fileInputStream)
}
else -> {
null
}
}
val streamedMessage = if (input != null) {
val kryo = endPoint.serialization.takeRead()
try {
kryo.read(connection, input)
} catch (e: Exception) {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "Error deserializing message from received streaming content, stream $streamId"
// either client or server. No other choices. We create an exception, because it's more useful!
throw endPoint.newException(errorMessage, e)
} finally {
endPoint.serialization.putRead(kryo)
if (output is FileWriter) {
val fileName = "${config.appId}_${streamId}_${connection.id}.tmp"
val tempFileLocation = OS.TEMP_DIR.resolve(fileName)
tempFileLocation.delete()
}
}
} else {
null
}
if (streamedMessage == null) {
if (output is FileWriter) {
val fileName = "${config.appId}_${streamId}_${connection.id}.tmp"
val tempFileLocation = OS.TEMP_DIR.resolve(fileName)
tempFileLocation.delete()
}
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "Error while processing streaming content, stream $streamId was null."
// either client or server. No other choices. We create an exception, because it's more useful!
throw endPoint.newException(errorMessage)
}
// this can be a regular message or an RMI message. Redispatch!
endPoint.processMessageFromChannel(connection, streamedMessage)
}
StreamingState.FAILED -> {
val output = streamingDataInMemory.remove(streamId)
if (output is FileWriter) {
val fileName = "${config.appId}_${streamId}_${connection.id}.tmp"
val tempFileLocation = OS.TEMP_DIR.resolve(fileName)
tempFileLocation.delete()
}
// clear all state
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
@ -151,13 +262,19 @@ internal class StreamingManager<CONNECTION : Connection>(private val logger: KLo
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +2 because we do not want to see the stack for the abstract `newException`
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 2)
exception.cleanStackTrace(3)
throw exception
}
StreamingState.UNKNOWN -> {
val output = streamingDataInMemory.remove(streamId)
if (output is FileWriter) {
val fileName = "${config.appId}_${streamId}_${connection.id}.tmp"
val tempFileLocation = OS.TEMP_DIR.resolve(fileName)
tempFileLocation.delete()
}
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "Unknown failure while receiving streaming content for stream $streamId"
@ -165,29 +282,29 @@ internal class StreamingManager<CONNECTION : Connection>(private val logger: KLo
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +2 because we do not want to see the stack for the abstract `newException`
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 2)
exception.cleanStackTrace(3)
throw exception
}
}
}
/**
* NOTE: MUST BE ON THE AERON THREAD!
* NOTE: MUST BE ON THE AERON THREAD BECAUSE THIS MUST BE SINGLE THREADED!!!
*
* Reassemble/figure out the internal message pieces
*
* NOTE sending a huge file can prevent other other network traffic from arriving until it's done!
* NOTE sending a huge file can cause other network traffic delays!
*/
fun processDataMessage(message: StreamingData, endPoint: EndPoint<CONNECTION>) {
fun processDataMessage(message: StreamingData, endPoint: EndPoint<CONNECTION>, connection: CONNECTION) {
// the receiving data will ALWAYS come sequentially, but there might be OTHER streaming data received meanwhile.
val streamId = message.streamId
// NOTE: the stream session ID is a combination of the connection ID + random ID (on the receiving side)
val streamId = (connection.id.toLong() shl 4) or message.streamId.toLong()
val controlMessage = streamingDataTarget[streamId]
if (controlMessage != null) {
streamingDataInMemory.getOrPut(streamId) { AeronOutput() }.writeBytes(message.payload!!)
val dataWriter = streamingDataInMemory[streamId]
if (dataWriter != null) {
dataWriter.writeBytes(message.startPosition, message.payload!!)
} else {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
@ -196,23 +313,25 @@ internal class StreamingManager<CONNECTION : Connection>(private val logger: KLo
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +2 because we do not want to see the stack for the abstract `newException`
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 5)
exception.cleanStackTrace(3)
throw exception
}
}
private fun sendFailMessageAndThrow(
e: Exception,
streamSessionId: Long,
streamSessionId: Int,
publication: Publication,
endPoint: EndPoint<CONNECTION>,
connection: CONNECTION
sendIdleStrategy: IdleStrategy,
connection: CONNECTION,
kryo: KryoWriter<CONNECTION>
) {
val failMessage = StreamingControl(StreamingState.FAILED, streamSessionId)
val failSent = endPoint.send(failMessage, publication, connection)
val failMessage = StreamingControl(StreamingState.FAILED, false, streamSessionId)
val failSent = endPoint.writeUnsafe(failMessage, publication, sendIdleStrategy, connection, kryo)
if (!failSent) {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
@ -221,10 +340,9 @@ internal class StreamingManager<CONNECTION : Connection>(private val logger: KLo
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +2 because we do not want to see the stack for the abstract `newException`
// +4 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 6)
exception.cleanStackTrace(4)
throw exception
} else {
// send it up!
@ -241,84 +359,103 @@ internal class StreamingManager<CONNECTION : Connection>(private val logger: KLo
* We don't write max possible length per message, we write out MTU (payload) length (so aeron doesn't fragment the message).
* The max possible length is WAY, WAY more than the max payload length.
*
* @param internalBuffer this is the ORIGINAL object data that is to be "chunked" and sent across the wire
* @return true if ALL the message chunks were successfully sent by aeron, false otherwise. Exceptions are caught and rethrown!
* @param originalBuffer this is the ORIGINAL object data that is to be blocks sent across the wire
*
* @return true if ALL the message blocks were successfully sent by aeron, false otherwise. Exceptions are caught and rethrown!
*/
fun send(
publication: Publication,
internalBuffer: MutableDirectBuffer,
originalBuffer: MutableDirectBuffer,
maxMessageSize: Int,
objectSize: Int,
endPoint: EndPoint<CONNECTION>,
connection: CONNECTION): Boolean {
kryo: KryoWriter<CONNECTION>,
sendIdleStrategy: IdleStrategy,
connection: CONNECTION
): Boolean {
// NOTE: our max object size for IN-MEMORY messages is an INT. For file transfer it's a LONG (so everything here is cast to a long)
var remainingPayload = objectSize
var payloadSent = 0
val streamSessionId = random.nextLong()
// NOTE: the stream session ID is a combination of the connection ID + random ID (on the receiving side)
val streamSessionId = CryptoManagement.secureRandom.nextInt()
// tell the other side how much data we are sending
val startMessage = StreamingControl(StreamingState.START, streamSessionId, objectSize.toLong())
val startSent = endPoint.send(startMessage, publication, connection)
val startMessage = StreamingControl(StreamingState.START, false, streamSessionId, remainingPayload.toLong())
val startSent = endPoint.writeUnsafe(startMessage, publication, sendIdleStrategy, connection, kryo)
if (!startSent) {
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "[${publication.sessionId()}] Error starting streaming content."
val errorMessage = "[${publication.sessionId()}] Error starting streaming content (could not send data)."
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +2 because we do not want to see the stack for the abstract `newException`
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 5)
exception.cleanStackTrace(3)
throw exception
}
val kryo: KryoExtra<CONNECTION> = endPoint.serialization.takeKryo()
// we do the FIRST chunk super-weird, because of the way we copy data around (we inject headers,
// we do the FIRST block super-weird, because of the way we copy data around (we inject headers,
// so the first message is SUPER tiny and is a COPY, the rest are no-copy.
// This is REUSED to prevent garbage collection issues.
val chunkData = StreamingData(streamSessionId)
// payload size is for a PRODUCER, and not SUBSCRIBER, so we have to include this amount every time.
// MINOR fragmentation by aeron is OK, since that will greatly speed up data transfer rates!
// the maxPayloadLength MUST ABSOLUTELY be less that the max size + header!
var sizeOfPayload = publication.maxMessageLength() - 200
var sizeOfBlockData = maxMessageSize
val header: ByteArray
val headerSize: Int
try {
val objectBuffer = kryo.write(connection, chunkData)
// This is REUSED to prevent garbage collection issues.
val blockData = StreamingData(streamSessionId)
val objectBuffer = kryo.write(connection, blockData)
headerSize = objectBuffer.position()
header = ByteArray(headerSize)
// we have to account for the header + the MAX optimized int size
sizeOfPayload -= (headerSize + 5)
// we have to account for the header + the MAX optimized int size (position and data-length)
val dataSize = headerSize + 5 + 5
sizeOfBlockData -= dataSize
// this size might be a LITTLE too big, but that's ok, since we only make this specific buffer once.
val chunkBuffer = AeronOutput(headerSize + sizeOfPayload)
val blockBuffer = AeronOutput(dataSize)
// copy out our header info
objectBuffer.internalBuffer.getBytes(0, header, 0, headerSize)
// write out our header
chunkBuffer.writeBytes(header)
blockBuffer.writeBytes(header)
// write out the payload size using optimized data structures.
val varIntSize = chunkBuffer.writeVarInt(sizeOfPayload, true)
// write out the start-position (of the payload). First start-position is always 0
val positionIntSize = blockBuffer.writeVarInt(0, true)
// write out the payload size
val payloadIntSize = blockBuffer.writeVarInt(sizeOfBlockData, true)
// write out the payload. Our resulting data written out is the ACTUAL MTU of aeron.
internalBuffer.getBytes(0, chunkBuffer.internalBuffer, headerSize + varIntSize, sizeOfPayload)
originalBuffer.getBytes(0, blockBuffer.internalBuffer, headerSize + positionIntSize + payloadIntSize, sizeOfBlockData)
remainingPayload -= sizeOfPayload
payloadSent += sizeOfPayload
remainingPayload -= sizeOfBlockData
payloadSent += sizeOfBlockData
// we reuse/recycle objects, so the payload size is not EXACTLY what is specified
val reusedPayloadSize = headerSize + positionIntSize + payloadIntSize + sizeOfBlockData
val success = endPoint.aeronDriver.send(
publication = publication,
internalBuffer = blockBuffer.internalBuffer,
bufferClaim = kryo.bufferClaim,
offset = 0,
objectSize = reusedPayloadSize,
sendIdleStrategy = sendIdleStrategy,
connection = connection,
abortEarly = false,
listenerManager = endPoint.listenerManager
)
val success = endPoint.sendData(publication, chunkBuffer.internalBuffer, 0, headerSize + varIntSize + sizeOfPayload, connection)
if (!success) {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
@ -327,25 +464,22 @@ internal class StreamingManager<CONNECTION : Connection>(private val logger: KLo
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +2 because we do not want to see the stack for the abstract `newException`
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 5)
exception.cleanStackTrace(3)
throw exception
}
} catch (e: Exception) {
sendFailMessageAndThrow(e, streamSessionId, publication, endPoint, connection)
sendFailMessageAndThrow(e, streamSessionId, publication, endPoint, sendIdleStrategy, connection, kryo)
return false // doesn't actually get here because exceptions are thrown, but this makes the IDE happy.
} finally {
endPoint.serialization.returnKryo(kryo)
}
// now send the chunks as fast as possible. Aeron will have us back-off if we send too quickly
// now send the block as fast as possible. Aeron will have us back-off if we send too quickly
while (remainingPayload > 0) {
val amountToSend = if (remainingPayload < sizeOfPayload) {
val amountToSend = if (remainingPayload < sizeOfBlockData) {
remainingPayload
} else {
sizeOfPayload
sizeOfBlockData
}
remainingPayload -= amountToSend
@ -358,35 +492,56 @@ internal class StreamingManager<CONNECTION : Connection>(private val logger: KLo
// fortunately, the way that serialization works, we can safely ADD data to the tail and then appropriately read it off
// on the receiving end without worry.
/// TODO: Compression/encryption??
try {
val varIntSize = OptimizeUtilsByteBuf.intLength(sizeOfPayload, true)
val writeIndex = payloadSent - headerSize - varIntSize
val positionIntSize = OptimizeUtilsByteBuf.intLength(payloadSent, true)
val payloadIntSize = OptimizeUtilsByteBuf.intLength(amountToSend, true)
val writeIndex = payloadSent - headerSize - positionIntSize - payloadIntSize
// write out our header data (this will OVERWRITE previous data!)
internalBuffer.putBytes(writeIndex, header)
originalBuffer.putBytes(writeIndex, header)
// write out the payload size using optimized data structures.
writeVarInt(internalBuffer, writeIndex + headerSize, sizeOfPayload, true)
// write out the payload start position
writeVarInt(originalBuffer, writeIndex + headerSize, payloadSent, true)
// write out the payload size
writeVarInt(originalBuffer, writeIndex + headerSize + positionIntSize, amountToSend, true)
// we reuse/recycle objects, so the payload size is not EXACTLY what is specified
val reusedPayloadSize = headerSize + payloadIntSize + positionIntSize + amountToSend
// write out the payload
endPoint.sendData(publication, internalBuffer, writeIndex, headerSize + varIntSize + amountToSend, connection)
val success = endPoint.aeronDriver.send(
publication = publication,
internalBuffer = originalBuffer,
bufferClaim = kryo.bufferClaim,
offset = writeIndex,
objectSize = reusedPayloadSize,
sendIdleStrategy = sendIdleStrategy,
connection = connection,
abortEarly = false,
listenerManager = endPoint.listenerManager
)
if (!success) {
// critical errors have an exception. Normal "the connection is closed" do not.
return false
}
payloadSent += amountToSend
} catch (e: Exception) {
val failMessage = StreamingControl(StreamingState.FAILED, streamSessionId)
val failSent = endPoint.send(failMessage, publication, connection)
val failMessage = StreamingControl(StreamingState.FAILED, false, streamSessionId)
val failSent = endPoint.writeUnsafe(failMessage, publication, sendIdleStrategy, connection, kryo)
if (!failSent) {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "[${publication.sessionId()}] Abnormal failure while streaming content."
val errorMessage = "[${publication.sessionId()}] Abnormal failure with exception while streaming content."
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +2 because we do not want to see the stack for the abstract `newException`
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 5)
val exception = endPoint.newException(errorMessage, e)
exception.cleanAllStackTrace()
throw exception
} else {
// send it up!
@ -395,8 +550,249 @@ internal class StreamingManager<CONNECTION : Connection>(private val logger: KLo
}
}
// send the last chunk of data
val finishedMessage = StreamingControl(StreamingState.FINISHED, streamSessionId, payloadSent.toLong())
return endPoint.send(finishedMessage, publication, connection)
// send the last block of data
val finishedMessage = StreamingControl(StreamingState.FINISHED, false, streamSessionId, payloadSent.toLong())
return endPoint.writeUnsafe(finishedMessage, publication, sendIdleStrategy, connection, kryo)
}
/**
* This is called ONLY when a message is too large to send across the network in a single message (large data messages should
* be split into smaller ones anyways!)
*
* NOTE: this **MUST** stay on the same co-routine that calls "send". This cannot be re-dispatched onto a different coroutine!
*
* We don't write max possible length per message, we write out MTU (payload) length (so aeron doesn't fragment the message).
* The max possible length is WAY, WAY more than the max payload length.
*
* @param streamSessionId the stream session ID is a combination of the connection ID + random ID (on the receiving side)
*
* @return true if ALL the message blocks were successfully sent by aeron, false otherwise. Exceptions are caught and rethrown!
*/
@Suppress("SameParameterValue")
fun sendFile(
file: File,
publication: Publication,
endPoint: EndPoint<CONNECTION>,
kryo: KryoWriter<CONNECTION>,
sendIdleStrategy: IdleStrategy,
connection: CONNECTION,
streamSessionId: Int
): Boolean {
val maxMessageSize = connection.maxMessageSize.toLong()
val fileInputStream = file.inputStream()
// if the message is a file, we xfer the file AS a file, and leave it as a temp file (with a file reference to it) on the remote endpoint
// the temp file will be unique.
// NOTE: our max object size for IN-MEMORY messages is an INT. For file transfer it's a LONG (so everything here is cast to a long)
var remainingPayload = file.length()
var payloadSent = 0
// tell the other side how much data we are sending
val startMessage = StreamingControl(StreamingState.START, true, streamSessionId, remainingPayload)
val startSent = endPoint.writeUnsafe(startMessage, publication, sendIdleStrategy, connection, kryo)
if (!startSent) {
fileInputStream.close()
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "[${publication.sessionId()}] Error starting streaming file."
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
exception.cleanStackTrace(3)
throw exception
}
// we do the FIRST block super-weird, because of the way we copy data around (we inject headers),
// so the first message is SUPER tiny and is a COPY, the rest are no-copy.
// payload size is for a PRODUCER, and not SUBSCRIBER, so we have to include this amount every time.
// we don't know which is larger, the max message size or the file size!
var sizeOfBlockData = maxMessageSize.coerceAtMost(remainingPayload).toInt()
val headerSize: Int
val buffer: ByteArray
val blockBuffer: UnsafeBuffer
try {
// This is REUSED to prevent garbage collection issues.
val blockData = StreamingData(streamSessionId)
val objectBuffer = kryo.write(connection, blockData)
headerSize = objectBuffer.position()
// we have to account for the header + the MAX optimized int size (position and data-length)
val dataSize = headerSize + 5 + 5
sizeOfBlockData -= dataSize
// this size might be a LITTLE too big, but that's ok, since we only make this specific buffer once.
buffer = ByteArray(sizeOfBlockData + dataSize)
blockBuffer = UnsafeBuffer(buffer)
// copy out our header info (this skips the header object)
objectBuffer.internalBuffer.getBytes(0, buffer, 0, headerSize)
// write out the start-position (of the payload). First start-position is always 0
val positionIntSize = OptimizeUtilsByteArray.writeInt(buffer, 0, true, headerSize)
// write out the payload size
val payloadIntSize = OptimizeUtilsByteArray.writeInt(buffer, sizeOfBlockData, true, headerSize + positionIntSize)
// write out the payload. Our resulting data written out is the ACTUAL MTU of aeron.
val readBytes = fileInputStream.read(buffer, headerSize + positionIntSize + payloadIntSize, sizeOfBlockData)
if (readBytes != sizeOfBlockData) {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "[${publication.sessionId()}] Abnormal failure while streaming file (read bytes was wrong! ${readBytes} - ${sizeOfBlockData}."
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
exception.cleanStackTrace(3)
throw exception
}
remainingPayload -= sizeOfBlockData
payloadSent += sizeOfBlockData
// we reuse/recycle objects, so the payload size is not EXACTLY what is specified
val reusedPayloadSize = headerSize + positionIntSize + payloadIntSize + sizeOfBlockData
val success = endPoint.aeronDriver.send(
publication = publication,
internalBuffer = blockBuffer,
bufferClaim = kryo.bufferClaim,
offset = 0,
objectSize = reusedPayloadSize,
sendIdleStrategy = sendIdleStrategy,
connection = connection,
abortEarly = false,
listenerManager = endPoint.listenerManager
)
if (!success) {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "[${publication.sessionId()}] Abnormal failure while streaming file."
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
exception.cleanStackTrace(3)
throw exception
}
} catch (e: Exception) {
fileInputStream.close()
sendFailMessageAndThrow(e, streamSessionId, publication, endPoint, sendIdleStrategy, connection, kryo)
return false // doesn't actually get here because exceptions are thrown, but this makes the IDE happy.
}
val aeronDriver = endPoint.aeronDriver
val listenerManager = endPoint.listenerManager
// now send the block as fast as possible. Aeron will have us back-off if we send too quickly
while (remainingPayload > 0) {
val amountToSend = if (remainingPayload < sizeOfBlockData) {
remainingPayload.toInt()
} else {
sizeOfBlockData
}
remainingPayload -= amountToSend
// to properly do this, we have to be careful with the underlying protocol, in order to avoid copying the buffer multiple times.
// the data that will be sent is object data + buffer data. We are sending the SAME parent buffer, just at different spots and
// with different headers -- so we don't copy out the data repeatedly
// fortunately, the way that serialization works, we can safely ADD data to the tail and then appropriately read it off
// on the receiving end without worry.
/// TODO: Compression/encryption??
try {
// write out the payload start position
val positionIntSize = OptimizeUtilsByteArray.writeInt(buffer, payloadSent, true, headerSize)
// write out the payload size
val payloadIntSize = OptimizeUtilsByteArray.writeInt(buffer, amountToSend, true, headerSize + positionIntSize)
// write out the payload. Our resulting data written out is the ACTUAL MTU of aeron.
val readBytes = fileInputStream.read(buffer, headerSize + positionIntSize + payloadIntSize, amountToSend)
if (readBytes != amountToSend) {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "[${publication.sessionId()}] Abnormal failure while streaming file (read bytes was wrong! ${readBytes} - ${amountToSend}."
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
exception.cleanStackTrace(3)
throw exception
}
// we reuse/recycle objects, so the payload size is not EXACTLY what is specified
val reusedPayloadSize = headerSize + positionIntSize + payloadIntSize + amountToSend
// write out the payload
aeronDriver.send(
publication = publication,
internalBuffer = blockBuffer,
bufferClaim = kryo.bufferClaim,
offset = 0, // 0 because we are not reading the entire file at once
objectSize = reusedPayloadSize,
sendIdleStrategy = sendIdleStrategy,
connection = connection,
abortEarly = false,
listenerManager = listenerManager
)
payloadSent += amountToSend
} catch (e: Exception) {
fileInputStream.close()
val failMessage = StreamingControl(StreamingState.FAILED, false, streamSessionId)
val failSent = endPoint.writeUnsafe(failMessage, publication, sendIdleStrategy, connection, kryo)
if (!failSent) {
// something SUPER wrong!
// more critical error sending the message. we shouldn't retry or anything.
val errorMessage = "[${publication.sessionId()}] Abnormal failure while streaming content."
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = endPoint.newException(errorMessage)
// +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
exception.cleanStackTrace(3)
throw exception
} else {
// send it up!
throw e
}
}
}
fileInputStream.close()
// send the last block of data
val finishedMessage = StreamingControl(StreamingState.FINISHED, true, streamSessionId, payloadSent.toLong())
return endPoint.writeUnsafe(finishedMessage, publication, sendIdleStrategy, connection, kryo)
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.streaming
interface StreamingWriter {
fun writeBytes(startPosition: Int, bytes: ByteArray)
fun isFinished(): Boolean
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.streaming;

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 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.connectionType;

View File

@ -1,43 +0,0 @@
/*
* 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.coroutines;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import kotlin.coroutines.Continuation;
import kotlin.jvm.functions.Function1;
/**
* Class to access suspending invocation of methods from kotlin...
*
* ULTIMATELY, this is all java bytecode, and the bytecode signature here matches what kotlin expects. The generics type information is
* discarded at compile time.
*/
public
class SuspendFunctionTrampoline {
/**
* trampoline so we can access suspend functions correctly using reflection
*/
@SuppressWarnings("unchecked")
@Nullable
public static
Object invoke(@NotNull final Continuation<?> continuation, @NotNull final Object suspendFunction) throws Throwable {
Function1<? super Continuation<? super Object>, ?> suspendFunction1 = (Function1<? super Continuation<? super Object>, ?>) suspendFunction;
return suspendFunction1.invoke((Continuation<? super Object>) continuation);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,6 @@
package dorkbox.network.exceptions
/**
* A session/stream could not be allocated.
* A session/stream/resource could not be allocated.
*/
class AllocationException(message: String) : ServerException(message)

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised by the client handshake.
*/
open class ClientHandshakeException : ClientException {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised for errors when dispatching messages
*/
open class MessageDispatchException : Exception {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised for ping errors
*/
open class PingException : Exception {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised for RMI errors
*/
open class RMIException : Exception {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised for send-sync errors
*/
open class SendSyncException : Exception {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised for a serialization error.
*/
open class SerializationException : Exception {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised by the server handshake.
*/
open class ServerHandshakeException : ServerException {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised by the server handshake when it times out.
*/
open class ServerTimedoutException : ServerException {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised for a streaming error.
*/
open class StreamingException : Exception {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.exceptions
/**
* A port could not be allocated.
*/
class PortAllocationException(message: String) : ServerException(message)
class TimeoutException: Exception() {
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 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.exceptions
/**
* The type of exceptions raised when transmitting data.
*/
open class TransmitException : Exception {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 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.exceptions;

View File

@ -0,0 +1,254 @@
/*
* Copyright 2024 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.handshake
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.AeronDriver.Companion.getLocalAddressString
import dorkbox.network.aeron.AeronDriver.Companion.uri
import dorkbox.network.aeron.controlEndpoint
import dorkbox.network.aeron.endpoint
import dorkbox.network.connection.EndPoint
import dorkbox.network.exceptions.ClientRetryException
import dorkbox.network.exceptions.ClientTimedOutException
import io.aeron.CommonContext
import kotlinx.atomicfu.AtomicBoolean
import java.net.Inet4Address
import java.net.InetAddress
/**
* Set up the subscription + publication channels to the server
*
* Note: this class is NOT closed the traditional way! It's pub/sub objects are used by the connection (which is where they are closed)
*
* @throws ClientRetryException if we need to retry to connect
* @throws ClientTimedOutException if we cannot connect to the server in the designated time
*/
internal class ClientConnectionDriver(val connectionInfo: PubSub) {
companion object {
fun build(
shutdown: AtomicBoolean,
aeronDriver: AeronDriver,
handshakeTimeoutNs: Long,
handshakeConnection: ClientHandshakeDriver,
connectionInfo: ClientConnectionInfo,
port2Server: Int, // this is the port2 value from the server
tagName: String
): ClientConnectionDriver {
val handshakePubSub = handshakeConnection.pubSub
val reliable = handshakePubSub.reliable
// flipped because we are connecting to these!
val sessionIdPub = connectionInfo.sessionIdSub
val sessionIdSub = connectionInfo.sessionIdPub
val streamIdPub = connectionInfo.streamIdSub
val streamIdSub = connectionInfo.streamIdPub
val isUsingIPC = handshakePubSub.isIpc
val logInfo: String
val pubSub: PubSub
if (isUsingIPC) {
// Create a subscription at the given address and port, using the given stream ID.
logInfo = "CONNECTION-IPC"
pubSub = buildIPC(
shutdown = shutdown,
aeronDriver = aeronDriver,
handshakeTimeoutNs = handshakeTimeoutNs,
sessionIdPub = sessionIdPub,
sessionIdSub = sessionIdSub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
reliable = reliable,
tagName = tagName,
logInfo = logInfo
)
}
else {
val remoteAddress = handshakePubSub.remoteAddress
val remoteAddressString = handshakePubSub.remoteAddressString
val portPub = handshakePubSub.portPub
val portSub = handshakePubSub.portSub
logInfo = if (remoteAddress is Inet4Address) {
"CONNECTION-IPv4"
} else {
"CONNECTION-IPv6"
}
pubSub = buildUDP(
shutdown = shutdown,
aeronDriver = aeronDriver,
handshakeTimeoutNs = handshakeTimeoutNs,
sessionIdPub = sessionIdPub,
sessionIdSub = sessionIdSub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
remoteAddress = remoteAddress!!,
remoteAddressString = remoteAddressString,
portPub = portPub,
portSub = portSub,
port2Server = port2Server,
reliable = reliable,
tagName = tagName,
logInfo = logInfo
)
}
return ClientConnectionDriver(pubSub)
}
@Throws(ClientTimedOutException::class)
private fun buildIPC(
shutdown: AtomicBoolean,
aeronDriver: AeronDriver,
handshakeTimeoutNs: Long,
sessionIdPub: Int,
sessionIdSub: Int,
streamIdPub: Int,
streamIdSub: Int,
reliable: Boolean,
tagName: String,
logInfo: String
): PubSub {
// on close, the publication CAN linger (in case a client goes away, and then comes back)
// AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param)
// Create a publication at the given address and port, using the given stream ID.
val publicationUri = uri(CommonContext.IPC_MEDIA, sessionIdPub, reliable)
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
// can throw an exception! We catch it in the calling class
val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, true)
// can throw an exception! We catch it in the calling class
// we actually have to wait for it to connect before we continue
aeronDriver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause ->
ClientTimedOutException("$logInfo publication cannot connect with server!", cause)
}
// Create a subscription at the given address and port, using the given stream ID.
val subscriptionUri = uri(CommonContext.IPC_MEDIA, sessionIdSub, reliable)
val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, true)
// wait for the REMOTE end to also connect to us!
aeronDriver.waitForConnection(shutdown, subscription, handshakeTimeoutNs, logInfo) { cause ->
ClientTimedOutException("$logInfo subscription cannot connect with server!", cause)
}
return PubSub(
pub = publication,
sub = subscription,
sessionIdPub = sessionIdPub,
sessionIdSub = sessionIdSub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
reliable = reliable,
remoteAddress = null,
remoteAddressString = EndPoint.IPC_NAME,
portPub = 0,
portSub = 0,
tagName = tagName
)
}
@Throws(ClientTimedOutException::class)
private fun buildUDP(
shutdown: AtomicBoolean,
aeronDriver: AeronDriver,
handshakeTimeoutNs: Long,
sessionIdPub: Int,
sessionIdSub: Int,
streamIdPub: Int,
streamIdSub: Int,
remoteAddress: InetAddress,
remoteAddressString: String,
portPub: Int,
portSub: Int,
port2Server: Int, // this is the port2 value from the server
reliable: Boolean,
tagName: String,
logInfo: String,
): PubSub {
val isRemoteIpv4 = remoteAddress is Inet4Address
// on close, the publication CAN linger (in case a client goes away, and then comes back)
// AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param)
// Create a publication at the given address and port, using the given stream ID.
val publicationUri = uri(CommonContext.UDP_MEDIA, sessionIdPub, reliable)
.endpoint(isRemoteIpv4, remoteAddressString, portPub)
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
// can throw an exception! We catch it in the calling class
val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, false)
// can throw an exception! We catch it in the calling class
// we actually have to wait for it to connect before we continue
aeronDriver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause ->
ClientTimedOutException("$logInfo publication cannot connect with server $remoteAddressString", cause)
}
// this will cause us to listen on the interface that connects with the remote address, instead of ALL interfaces.
val localAddressString = getLocalAddressString(publication, isRemoteIpv4)
// A control endpoint for the subscriptions will cause a periodic service management "heartbeat" to be sent to the
// remote endpoint publication, which permits the remote publication to send us data, thereby getting us around NAT
val subscriptionUri = uri(CommonContext.UDP_MEDIA, sessionIdSub, reliable)
.endpoint(isRemoteIpv4, localAddressString, portSub)
.controlEndpoint(isRemoteIpv4, remoteAddressString, port2Server)
.controlMode(CommonContext.MDC_CONTROL_MODE_DYNAMIC)
val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, false)
// wait for the REMOTE end to also connect to us!
aeronDriver.waitForConnection(shutdown, subscription, handshakeTimeoutNs, logInfo) { cause ->
ClientTimedOutException("$logInfo subscription cannot connect with server!", cause)
}
return PubSub(
pub = publication,
sub = subscription,
sessionIdPub = sessionIdPub,
sessionIdSub = sessionIdSub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
reliable = reliable,
remoteAddress = remoteAddress,
remoteAddressString = remoteAddressString,
portPub = portPub,
portSub = portSub,
tagName = tagName
)
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,9 +15,16 @@
*/
package dorkbox.network.handshake
internal class ClientConnectionInfo(val port: Int = 0,
val sessionId: Int,
val streamId: Int = 0,
val publicKey: ByteArray = ByteArray(0),
val kryoRegistrationDetails: ByteArray) {
}
import javax.crypto.spec.SecretKeySpec
internal class ClientConnectionInfo(
val sessionIdPub: Int = 0,
val sessionIdSub: Int = 0,
val streamIdPub: Int,
val streamIdSub: Int = 0,
val publicKey: ByteArray = ByteArray(0),
val sessionTimeout: Long,
val bufferedMessages: Boolean,
val kryoRegistrationDetails: ByteArray,
val secretKey: SecretKeySpec
)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2024 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,33 +16,32 @@
package dorkbox.network.handshake
import dorkbox.network.Client
import dorkbox.network.aeron.mediaDriver.MediaDriverClient
import dorkbox.network.connection.Connection
import dorkbox.network.connection.CryptoManagement
import dorkbox.network.connection.ListenerManager
import dorkbox.network.exceptions.ClientRejectedException
import dorkbox.network.exceptions.ClientTimedOutException
import dorkbox.network.exceptions.ServerException
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.ListenerManager.Companion.cleanAllStackTrace
import dorkbox.network.connection.ListenerManager.Companion.cleanStackTraceInternal
import dorkbox.network.exceptions.*
import dorkbox.util.Sys
import io.aeron.FragmentAssembler
import io.aeron.Image
import io.aeron.logbuffer.FragmentHandler
import io.aeron.logbuffer.Header
import mu.KLogger
import org.agrona.DirectBuffer
import java.lang.Thread.sleep
import java.util.concurrent.*
import org.slf4j.Logger
internal class ClientHandshake<CONNECTION: Connection>(
private val crypto: CryptoManagement,
private val endPoint: Client<CONNECTION>,
private val logger: KLogger
private val client: Client<CONNECTION>,
private val logger: Logger
) {
// @Volatile is used BECAUSE suspension of coroutines can continue on a DIFFERENT thread. We want to make sure that thread visibility is
// correct when this happens. There are no race-conditions to be wary of.
private val crypto = client.crypto
private val handler: FragmentHandler
private val pollIdleStrategy = endPoint.config.pollIdleStrategy.cloneToNormal()
private val handshaker = client.handshaker
// used to keep track and associate UDP/IPC handshakes between client/server
@Volatile
@ -61,28 +60,54 @@ internal class ClientHandshake<CONNECTION: Connection>(
private var failedException: Exception? = null
init {
// now we have a bi-directional connection with the server on the handshake "socket".
// NOTE: subscriptions (ie: reading from buffers, etc) are not thread safe! Because it is ambiguous HOW EXACTLY they are unsafe,
// we exclusively read from the DirectBuffer on a single thread.
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be:
// - long running
// - re-entrant with the client
handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
// this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe!
val sessionId = header.sessionId()
val streamId = header.streamId()
val aeronLogInfo = "$sessionId/$streamId"
val message = endPoint.readHandshakeMessage(buffer, offset, length, header, aeronLogInfo)
// note: this address will ALWAYS be an IP:PORT combo OR it will be aeron:ipc (if IPC, it will be a different handler!)
val remoteIpAndPort = (header.context() as Image).sourceIdentity()
// split
val splitPoint = remoteIpAndPort.lastIndexOf(':')
val clientAddressString = remoteIpAndPort.substring(0, splitPoint)
val logInfo = "$sessionId/$streamId:$clientAddressString"
failedException = null
needToRetry = false
// it must be a registration message
if (message !is HandshakeMessage) {
failedException = ClientRejectedException("[$aeronLogInfo] cancelled handshake for unrecognized message: $message")
return@FragmentAssembler
}
// ugh, this is verbose -- but necessary
val message = try {
val msg = handshaker.readMessage(buffer, offset, length)
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (msg !is HandshakeMessage) {
throw ClientRejectedException("[$logInfo] Connection not allowed! unrecognized message: $msg") .apply { cleanAllStackTrace() }
} else if (logger.isTraceEnabled) {
logger.trace("[$logInfo] (${msg.connectKey}) received HS: $msg")
}
msg
} catch (e: Exception) {
client.listenerManager.notifyError(ClientHandshakeException("[$logInfo] Error de-serializing handshake message!!", e))
null
} ?: return@FragmentAssembler
// this is an error message
if (message.state == HandshakeMessage.INVALID) {
val cause = ServerException(message.errorMessage ?: "Unknown").apply { stackTrace = stackTrace.copyOfRange(0, 1) }
failedException = ClientRejectedException("[$aeronLogInfo} - ${message.connectKey}] cancelled handshake", cause)
val cause = ServerException(message.errorMessage ?: "Unknown").apply { stackTrace = emptyArray() }
failedException = ClientRejectedException("[$logInfo}] (${message.connectKey}) cancelled handshake", cause)
.apply { cleanAllStackTrace() }
return@FragmentAssembler
}
@ -94,7 +119,7 @@ internal class ClientHandshake<CONNECTION: Connection>(
}
if (connectKey != message.connectKey) {
logger.error("[$aeronLogInfo - $connectKey] ignored handshake for ${message.connectKey} (Was for another client)")
logger.error("[$logInfo] ($connectKey) ignored handshake for ${message.connectKey} (Was for another client)")
return@FragmentAssembler
}
@ -110,28 +135,20 @@ internal class ClientHandshake<CONNECTION: Connection>(
if (registrationData != null && serverPublicKeyBytes != null) {
connectionHelloInfo = crypto.decrypt(registrationData, serverPublicKeyBytes)
} else {
failedException = ClientRejectedException("[$aeronLogInfo} - ${message.connectKey}] canceled handshake for message without registration and/or public key info")
failedException = ClientRejectedException("[$logInfo}] (${message.connectKey}) canceled handshake for message without registration and/or public key info")
.apply { cleanAllStackTrace() }
}
}
HandshakeMessage.HELLO_ACK_IPC -> {
// The message was intended for this client. Try to parse it as one of the available message types.
// this message is NOT-ENCRYPTED!
val cryptInput = crypto.cryptInput
val serverPublicKeyBytes = message.publicKey
if (registrationData != null) {
cryptInput.buffer = registrationData
val sessId = cryptInput.readInt()
val streamPubId = cryptInput.readInt()
val regDetailsSize = cryptInput.readInt()
val regDetails = cryptInput.readBytes(regDetailsSize)
// now read data off
connectionHelloInfo = ClientConnectionInfo(sessionId = sessId,
port = streamPubId,
kryoRegistrationDetails = regDetails)
if (registrationData != null && serverPublicKeyBytes != null) {
connectionHelloInfo = crypto.nocrypt(registrationData, serverPublicKeyBytes)
} else {
failedException = ClientRejectedException("[$aeronLogInfo - ${message.connectKey}] canceled handshake for message without registration data")
failedException = ClientRejectedException("[$logInfo}] (${message.connectKey}) canceled handshake for message without registration and/or public key info")
.apply { cleanAllStackTrace() }
}
}
HandshakeMessage.DONE_ACK -> {
@ -139,7 +156,8 @@ internal class ClientHandshake<CONNECTION: Connection>(
}
else -> {
val stateString = HandshakeMessage.toStateString(message.state)
failedException = ClientRejectedException("[$aeronLogInfo - ${message.connectKey}] cancelled handshake for message that is $stateString")
failedException = ClientRejectedException("[$logInfo] (${message.connectKey}) cancelled handshake for message that is $stateString")
.apply { cleanAllStackTrace() }
}
}
}
@ -149,76 +167,75 @@ internal class ClientHandshake<CONNECTION: Connection>(
* Make sure that NON-ZERO is returned
*/
private fun getSafeConnectKey(): Long {
var key = endPoint.crypto.secureRandom.nextLong()
var key = CryptoManagement.secureRandom.nextLong()
while (key == 0L) {
key = endPoint.crypto.secureRandom.nextLong()
key = CryptoManagement.secureRandom.nextLong()
}
return key
}
// called from the connect thread
fun hello(handshakeConnection: MediaDriverClient, connectionTimeoutSec: Int) : ClientConnectionInfo {
failedException = null
// when exceptions are thrown, the handshake pub/sub will be closed
fun hello(
tagName: String,
endPoint: EndPoint<CONNECTION>,
handshakeConnection: ClientHandshakeDriver,
handshakeTimeoutNs: Long
) : ClientConnectionInfo {
val pubSub = handshakeConnection.pubSub
// is our pub still connected??
if (!pubSub.pub.isConnected) {
throw ClientException("Handshake publication is not connected, and it is expected to be connected!")
}
// always make sure that we reset the state when we start (in the event of reconnects)
reset()
connectKey = getSafeConnectKey()
val publicKey = endPoint.storage.getPublicKey()!!
val aeronLogInfo = "${handshakeConnection.remoteSessionId}/${handshakeConnection.streamId}"
// Send the one-time pad to the server.
val publication = handshakeConnection.publication
val subscription = handshakeConnection.subscription
try {
endPoint.writeHandshakeMessage(publication, aeronLogInfo,
HandshakeMessage.helloFromClient(connectKey, publicKey,
handshakeConnection.localSessionId,
handshakeConnection.subscriptionPort,
handshakeConnection.subscription.streamId()))
// Send the one-time pad to the server.
handshaker.writeMessage(pubSub.pub, handshakeConnection.details,
HandshakeMessage.helloFromClient(
connectKey = connectKey,
publicKey = client.storage.publicKey,
streamIdSub = pubSub.streamIdSub,
portSub = pubSub.portSub,
tagName = tagName
))
} catch (e: Exception) {
subscription.close()
publication.close()
logger.error("[$aeronLogInfo] Handshake error!", e)
throw e
handshakeConnection.close(endPoint)
throw TransmitException("$handshakeConnection Handshake message error!", e)
}
// block until we receive the connection information from the server
var pollCount: Int
pollIdleStrategy.reset()
val timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong()) + endPoint.aeronDriver.getLingerNs()
val startTime = System.nanoTime()
while (System.nanoTime() - startTime < timoutInNanos) {
while (System.nanoTime() - startTime < handshakeTimeoutNs) {
// NOTE: regarding fragment limit size. Repeated calls to '.poll' will reassemble a fragment.
// `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)`
pollCount = subscription.poll(handler, 1)
pubSub.sub.poll(handler, 1)
if (failedException != null || connectionHelloInfo != null) {
if (endPoint.isShutdown() || failedException != null || connectionHelloInfo != null) {
break
}
// 0 means we idle. >0 means reset and don't idle (because there are likely more)
pollIdleStrategy.idle(pollCount)
Thread.sleep(100)
}
val failedEx = failedException
if (failedEx != null) {
// no longer necessary to hold this connection open (if not a failure, we close the handshake after the DONE message)
subscription.close()
publication.close()
handshakeConnection.close(endPoint)
ListenerManager.cleanStackTraceInternal(failedEx)
failedEx.cleanStackTraceInternal()
throw failedEx
}
if (connectionHelloInfo == null) {
// no longer necessary to hold this connection open (if not a failure, we close the handshake after the DONE message)
subscription.close()
publication.close()
handshakeConnection.close(endPoint)
val exception = ClientTimedOutException("[$aeronLogInfo] Waiting for registration response from server")
ListenerManager.cleanStackTraceInternal(exception)
val exception = ClientTimedOutException("$handshakeConnection Waiting for registration response from server for more than ${Sys.getTimePrettyFull(handshakeTimeoutNs)}")
throw exception
}
@ -226,39 +243,47 @@ internal class ClientHandshake<CONNECTION: Connection>(
}
// called from the connect thread
fun done(handshakeConnection: MediaDriverClient, connectionTimeoutSec: Int) {
val registrationMessage = HandshakeMessage.doneFromClient(connectKey,
handshakeConnection.subscriptionPort,
handshakeConnection.subscription.streamId())
// when exceptions are thrown, the handshake pub/sub will be closed
fun done(
endPoint: EndPoint<CONNECTION>,
handshakeConnection: ClientHandshakeDriver,
clientConnection: ClientConnectionDriver,
handshakeTimeoutNs: Long,
logInfo: String
) {
val pubSub = clientConnection.connectionInfo
val handshakePubSub = handshakeConnection.pubSub
val aeronLogInfo = "${handshakeConnection.remoteSessionId}/${handshakeConnection.streamId}"
// is our pub still connected??
if (!pubSub.pub.isConnected) {
throw ClientException("Handshake publication is not connected, and it is expected to be connected!")
}
// Send the done message to the server.
try {
endPoint.writeHandshakeMessage(handshakeConnection.publication, aeronLogInfo, registrationMessage)
handshaker.writeMessage(handshakeConnection.pubSub.pub, logInfo,
HandshakeMessage.doneFromClient(
connectKey = connectKey,
sessionIdSub = handshakePubSub.sessionIdSub,
streamIdSub = handshakePubSub.streamIdSub
))
} catch (e: Exception) {
handshakeConnection.subscription.close()
handshakeConnection.publication.close()
throw e
handshakeConnection.close(endPoint)
throw TransmitException("$handshakeConnection Handshake message error!", e)
}
// block until we receive the connection information from the server
failedException = null
pollIdleStrategy.reset()
connectionDone = false
var pollCount: Int
val subscription = handshakeConnection.subscription
val timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong())
// block until we receive the connection information from the server
var startTime = System.nanoTime()
while (System.nanoTime() - startTime < timoutInNanos) {
while (System.nanoTime() - startTime < handshakeTimeoutNs) {
// NOTE: regarding fragment limit size. Repeated calls to '.poll' will reassemble a fragment.
// `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)`
pollCount = subscription.poll(handler, 1)
handshakePubSub.sub.poll(handler, 1)
if (failedException != null || connectionDone) {
if (endPoint.isShutdown() || failedException != null || connectionDone) {
break
}
@ -269,20 +294,21 @@ internal class ClientHandshake<CONNECTION: Connection>(
startTime = System.nanoTime()
}
sleep(100L)
// 0 means we idle. >0 means reset and don't idle (because there are likely more)
pollIdleStrategy.idle(pollCount)
Thread.sleep(100)
}
val failedEx = failedException
if (failedEx != null) {
handshakeConnection.close(endPoint)
throw failedEx
}
if (!connectionDone) {
val exception = ClientTimedOutException("Waiting for registration response from server")
ListenerManager.cleanStackTraceInternal(exception)
// since this failed, close everything
handshakeConnection.close(endPoint)
val exception = ClientTimedOutException("Timed out waiting for registration response from server: ${Sys.getTimePrettyFull(handshakeTimeoutNs)}")
throw exception
}
}

View File

@ -0,0 +1,402 @@
/*
* Copyright 2024 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.handshake
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.AeronDriver.Companion.getLocalAddressString
import dorkbox.network.aeron.AeronDriver.Companion.streamIdAllocator
import dorkbox.network.aeron.AeronDriver.Companion.uri
import dorkbox.network.aeron.AeronDriver.Companion.uriHandshake
import dorkbox.network.aeron.controlEndpoint
import dorkbox.network.aeron.endpoint
import dorkbox.network.connection.CryptoManagement
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.ListenerManager.Companion.cleanAllStackTrace
import dorkbox.network.connection.ListenerManager.Companion.cleanStackTraceInternal
import dorkbox.network.exceptions.ClientException
import dorkbox.network.exceptions.ClientRetryException
import dorkbox.network.exceptions.ClientTimedOutException
import dorkbox.util.Sys
import io.aeron.CommonContext
import io.aeron.Subscription
import kotlinx.atomicfu.AtomicBoolean
import org.slf4j.Logger
import java.net.Inet4Address
import java.net.InetAddress
import java.util.*
/**
* Set up the subscription + publication channels to the server
*
* @throws ClientRetryException if we need to retry to connect
* @throws ClientTimedOutException if we cannot connect to the server in the designated time
*/
internal class ClientHandshakeDriver(
val aeronDriver: AeronDriver,
val pubSub: PubSub,
private val logInfo: String,
val details: String
) {
companion object {
fun build(
endpoint: EndPoint<*>,
aeronDriver: AeronDriver,
autoChangeToIpc: Boolean,
remoteAddress: InetAddress?,
remoteAddressString: String,
remotePort1: Int,
remotePort2: Int,
clientListenPort: Int,
handshakeTimeoutNs: Long,
connectionTimoutInNs: Long,
reliable: Boolean,
tagName: String,
logger: Logger
): ClientHandshakeDriver {
logger.trace("Starting client handshake")
var isUsingIPC = false
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")
}
isUsingIPC = true
}
var logInfo = ""
var details = ""
// this must be unique otherwise we CANNOT connect to the server!
val sessionIdPub = CryptoManagement.secureRandom.nextInt()
// with IPC, the aeron driver MUST be shared, so having a UNIQUE sessionIdPub/Sub is unnecessary.
// sessionIdPub = sessionIdAllocator.allocate()
// sessionIdSub = sessionIdAllocator.allocate()
// streamIdPub is assigned by ipc/udp directly
var streamIdPub: Int
val streamIdSub = streamIdAllocator.allocate() // sub stream ID so the server can comm back to the client
var pubSub: PubSub? = null
val timeoutInfo = if (connectionTimoutInNs > 0L) {
"[Handshake: ${Sys.getTimePrettyFull(handshakeTimeoutNs)}, Max connection attempt: ${Sys.getTimePrettyFull(connectionTimoutInNs)}]"
} else {
"[Handshake: ${Sys.getTimePrettyFull(handshakeTimeoutNs)}, Max connection attempt: Unlimited]"
}
val config = endpoint.config
val shutdown = endpoint.shutdown
if (isUsingIPC) {
streamIdPub = config.ipcId
logInfo = "HANDSHAKE-IPC"
details = logInfo
logger.info("Client connecting via IPC. $timeoutInfo")
try {
pubSub = buildIPC(
shutdown = shutdown,
aeronDriver = aeronDriver,
handshakeTimeoutNs = handshakeTimeoutNs,
sessionIdPub = sessionIdPub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
reliable = reliable,
tagName = tagName,
logInfo = logInfo
)
} catch (exception: Exception) {
logger.error("Error initializing IPC connection", exception)
// MAYBE the server doesn't have IPC enabled? If no, we need to connect via network instead
isUsingIPC = false
// we will retry!
if (remoteAddress == null) {
// the exception will HARD KILL the client, make sure aeron driver is closed.
aeronDriver.close()
// 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", exception)
clientException.cleanStackTraceInternal()
throw clientException
}
}
}
if (!isUsingIPC) {
if (remoteAddress == null) {
val clientException = ClientException("Unable to connect via UDP to server. No address specified!")
clientException.cleanStackTraceInternal()
throw clientException
}
logInfo = if (remoteAddress is Inet4Address) {
"HANDSHAKE-IPv4"
} else {
"HANDSHAKE-IPv6"
}
streamIdPub = config.udpId
if (remoteAddress is Inet4Address) {
logger.info("Client connecting to IPv4 $remoteAddressString. $timeoutInfo")
} else {
logger.info("Client connecting to IPv6 $remoteAddressString. $timeoutInfo")
}
pubSub = buildUDP(
shutdown = shutdown,
aeronDriver = aeronDriver,
handshakeTimeoutNs = handshakeTimeoutNs,
remoteAddress = remoteAddress,
remoteAddressString = remoteAddressString,
portPub = remotePort1,
portSub = clientListenPort,
port2Server = remotePort2,
sessionIdPub = sessionIdPub,
streamIdPub = streamIdPub,
reliable = reliable,
streamIdSub = streamIdSub,
tagName = tagName,
logInfo = logInfo
)
// we have to figure out what our sub port info is, otherwise the server cannot connect back!
val subscriptionAddress = try {
getLocalAddressString(pubSub.sub)
} catch (e: Exception) {
throw ClientRetryException("$logInfo subscription is not properly created!", e)
}
details = if (subscriptionAddress == remoteAddressString) {
logInfo
} else {
"$logInfo $subscriptionAddress -> $remoteAddressString"
}
}
return ClientHandshakeDriver(aeronDriver, pubSub!!, logInfo, details)
}
@Throws(ClientTimedOutException::class)
private fun buildIPC(
shutdown: AtomicBoolean,
aeronDriver: AeronDriver,
handshakeTimeoutNs: Long,
sessionIdPub: Int,
streamIdPub: Int,
streamIdSub: Int,
reliable: Boolean,
tagName: String,
logInfo: String,
): PubSub {
// Create a publication at the given address and port, using the given stream ID.
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
val publicationUri = uri(CommonContext.IPC_MEDIA, sessionIdPub, reliable)
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
// For publications, if we add them "too quickly" (faster than the 'linger' timeout), Aeron will throw exceptions.
// ESPECIALLY if it is with the same streamID
// this check is in the "reconnect" logic
// can throw an exception! We catch it in the calling class
val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, true)
// can throw an exception! We catch it in the calling class
// we actually have to wait for it to connect before we continue
aeronDriver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause ->
ClientTimedOutException("$logInfo publication cannot connect with server in ${Sys.getTimePrettyFull(handshakeTimeoutNs)}", cause)
}
// Create a subscription at the given address and port, using the given stream ID.
val subscriptionUri = uriHandshake(CommonContext.IPC_MEDIA, reliable)
val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, true)
return PubSub(
pub = publication,
sub = subscription,
sessionIdPub = sessionIdPub,
sessionIdSub = 0,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
reliable = reliable,
remoteAddress = null,
remoteAddressString = EndPoint.IPC_NAME,
portPub = 0,
portSub = 0,
tagName = tagName
)
}
@Throws(ClientTimedOutException::class)
private fun buildUDP(
shutdown: AtomicBoolean,
aeronDriver: AeronDriver,
handshakeTimeoutNs: Long,
remoteAddress: InetAddress,
remoteAddressString: String,
portPub: Int, // this is the port1 value from the server
portSub: Int,
port2Server: Int, // this is the port2 value from the server
sessionIdPub: Int,
streamIdPub: Int,
reliable: Boolean,
streamIdSub: Int,
tagName: String,
logInfo: String,
): PubSub {
@Suppress("NAME_SHADOWING")
var portSub = portSub
// on close, the publication CAN linger (in case a client goes away, and then comes back)
// AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param)
val isRemoteIpv4 = remoteAddress is Inet4Address
// Create a publication at the given address and port, using the given stream ID.
// ANY sessionID for the publication will work, because the SERVER doesn't have it defined
val publicationUri = uri(CommonContext.UDP_MEDIA, sessionIdPub, reliable)
.endpoint(isRemoteIpv4, remoteAddressString, portPub)
// For publications, if we add them "too quickly" (faster than the 'linger' timeout), Aeron will throw exceptions.
// ESPECIALLY if it is with the same streamID. This was noticed as a problem with IPC
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
// can throw an exception! We catch it in the calling class
val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, false)
// can throw an exception! We catch it in the calling class
// we actually have to wait for it to connect before we continue
aeronDriver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause ->
streamIdAllocator.free(streamIdSub) // we don't continue, so close this as well
ClientTimedOutException("$logInfo publication cannot connect with server in ${Sys.getTimePrettyFull(handshakeTimeoutNs)}", cause)
}
// this will cause us to listen on the interface that connects with the remote address, instead of ALL interfaces.
val localAddressString = getLocalAddressString(publication, isRemoteIpv4)
// Create a subscription the given address and port, using the given stream ID.
var subscription: Subscription? = null
if (portSub > -1) {
// this means we have EXPLICITLY defined a port, we must try to use it
// A control endpoint for the subscriptions will cause a periodic service management "heartbeat" to be sent to the
// remote endpoint publication, which permits the remote publication to send us data, thereby getting us around NAT
val subscriptionUri = uriHandshake(CommonContext.UDP_MEDIA, reliable)
.endpoint(isRemoteIpv4, localAddressString, portSub)
.controlEndpoint(isRemoteIpv4, remoteAddressString, port2Server)
.controlMode(CommonContext.MDC_CONTROL_MODE_DYNAMIC)
subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, false)
} else {
// randomly select what port should be used
var retryCount = 100
val random = CryptoManagement.secureRandom
val isSameMachine = remoteAddress.isLoopbackAddress || remoteAddress == EndPoint.lanAddress
portSub = random.nextInt(Short.MAX_VALUE-1025) + 1025
while (subscription == null && retryCount-- > 0) {
// find a random port to bind to if we are loopback OR if we are the same IP address (not loopback, but to ourselves)
if (isSameMachine) {
// range from 1025-65534
portSub = random.nextInt(Short.MAX_VALUE-1025) + 1025
}
try {
// A control endpoint for the subscriptions will cause a periodic service management "heartbeat" to be sent to the
// remote endpoint publication, which permits the remote publication to send us data, thereby getting us around NAT
val subscriptionUri = uriHandshake(CommonContext.UDP_MEDIA, reliable)
.endpoint(isRemoteIpv4, localAddressString, portSub)
.controlEndpoint(isRemoteIpv4, remoteAddressString, port2Server)
.controlMode(CommonContext.MDC_CONTROL_MODE_DYNAMIC)
subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, false)
} catch (ignored: Exception) {
// whoops keep retrying!!
}
}
}
if (subscription == null) {
val ex = ClientTimedOutException("Cannot create subscription port $logInfo. All attempted ports are invalid")
ex.cleanAllStackTrace()
throw ex
}
return PubSub(
pub = publication,
sub = subscription,
sessionIdPub = sessionIdPub,
sessionIdSub = 0,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
reliable = reliable,
remoteAddress = remoteAddress,
remoteAddressString = remoteAddressString,
portPub = portPub,
portSub = portSub,
tagName = tagName
)
}
}
fun close(endpoint: EndPoint<*>) {
// only the subs are allocated on the client!
// sessionIdAllocator.free(pubSub.sessionIdPub)
// sessionIdAllocator.free(sessionIdSub)
// streamIdAllocator.free(streamIdPub)
streamIdAllocator.free(pubSub.streamIdSub)
// on close, we want to make sure this file is DELETED!
// we might not be able to close these connections.
try {
aeronDriver.close(pubSub.sub, logInfo)
}
catch (e: Exception) {
endpoint.listenerManager.notifyError(e)
}
try {
aeronDriver.close(pubSub.pub, logInfo)
}
catch (e: Exception) {
endpoint.listenerManager.notifyError(e)
}
}
}

View File

@ -1,3 +1,19 @@
/*
* Copyright 2023 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.handshake
import org.agrona.collections.Object2IntHashMap
@ -9,18 +25,22 @@ import java.net.InetAddress
internal class ConnectionCounts {
private val connectionsPerIpCounts = Object2IntHashMap<InetAddress>(-1)
@Synchronized
fun get(inetAddress: InetAddress): Int {
return connectionsPerIpCounts.getOrPut(inetAddress) { 0 }
}
@Synchronized
fun increment(inetAddress: InetAddress, currentCount: Int) {
connectionsPerIpCounts[inetAddress] = currentCount + 1
}
@Synchronized
fun decrement(inetAddress: InetAddress, currentCount: Int) {
connectionsPerIpCounts[inetAddress] = currentCount - 1
}
@Synchronized
fun decrementSlow(inetAddress: InetAddress) {
if (connectionsPerIpCounts.containsKey(inetAddress)) {
val defaultVal = connectionsPerIpCounts.getValue(inetAddress)
@ -28,11 +48,13 @@ internal class ConnectionCounts {
}
}
@Synchronized
fun isEmpty(): Boolean {
return connectionsPerIpCounts.isEmpty()
}
@Synchronized
override fun toString(): String {
return connectionsPerIpCounts.entries.joinToString()
return connectionsPerIpCounts.entries.map { it.key }.joinToString()
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,18 +32,16 @@ internal class HandshakeMessage private constructor() {
// -1 means there is an error
var state = INVALID
// used to name a connection (via the client)
var tag: String = ""
var errorMessage: String? = null
var subscriptionPort = 0
var port = 0
var streamId = 0
var sessionId = 0
// by default, this will be a reliable connection. When the client connects to the server, the client will specify if the new connection
// is a reliable/unreliable connection when setting up the MediaDriverConnection
val isReliable = true
// the client sends its registration data to the server to make sure that the registered classes are the same between the client/server
var registrationData: ByteArray? = null
@ -56,14 +54,15 @@ internal class HandshakeMessage private constructor() {
const val DONE = 3
const val DONE_ACK = 4
fun helloFromClient(connectKey: Long, publicKey: ByteArray, sessionId: Int, subscriptionPort: Int, streamId: Int): HandshakeMessage {
fun helloFromClient(connectKey: Long, publicKey: ByteArray, streamIdSub: Int, portSub: Int, tagName: String): HandshakeMessage {
val hello = HandshakeMessage()
hello.state = HELLO
hello.connectKey = connectKey // this is 'bounced back' by the server, so the client knows if it's the correct connection message
hello.publicKey = publicKey
hello.sessionId = sessionId
hello.subscriptionPort = subscriptionPort
hello.streamId = streamId
hello.sessionId = 0 // not used by the server, since it connects in a different way!
hello.streamId = streamIdSub
hello.port = portSub
hello.tag = tagName
return hello
}
@ -81,12 +80,12 @@ internal class HandshakeMessage private constructor() {
return hello
}
fun doneFromClient(connectKey: Long, subscriptionPort: Int, streamId: Int): HandshakeMessage {
fun doneFromClient(connectKey: Long, sessionIdSub: Int, streamIdSub: Int): HandshakeMessage {
val hello = HandshakeMessage()
hello.state = DONE
hello.connectKey = connectKey // THIS MUST NEVER CHANGE! (the server/client expect this)
hello.subscriptionPort = subscriptionPort
hello.streamId = streamId
hello.sessionId = sessionIdSub
hello.streamId = streamIdSub
return hello
}
@ -134,6 +133,12 @@ internal class HandshakeMessage private constructor() {
", Error: $errorMessage"
}
return "HandshakeMessage($stateStr$errorMsg)"
val connectInfo = if (connectKey != 0L) {
", key=$connectKey"
} else {
""
}
return "HandshakeMessage($tag :: $stateStr$errorMsg sessionId=$sessionId, streamId=$streamId, port=$port${connectInfo})"
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright 2023 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.handshake
import dorkbox.network.Configuration
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.connection.Connection
import dorkbox.network.connection.ListenerManager
import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace
import dorkbox.network.exceptions.ClientException
import dorkbox.network.exceptions.ServerException
import dorkbox.network.serialization.KryoReader
import dorkbox.network.serialization.KryoWriter
import dorkbox.network.serialization.Serialization
import io.aeron.Publication
import io.aeron.logbuffer.FrameDescriptor
import org.agrona.DirectBuffer
import org.agrona.concurrent.IdleStrategy
import org.slf4j.Logger
internal class Handshaker<CONNECTION : Connection>(
private val logger: Logger,
config: Configuration,
serialization: Serialization<CONNECTION>,
private val listenerManager: ListenerManager<CONNECTION>,
val aeronDriver: AeronDriver,
val newException: (String, Throwable?) -> Throwable
) {
private val handshakeReadKryo: KryoReader<CONNECTION>
private val handshakeWriteKryo: KryoWriter<CONNECTION>
private val handshakeSendIdleStrategy: IdleStrategy
init {
val maxMessageSize = FrameDescriptor.computeMaxMessageLength(config.publicationTermBufferLength)
// All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems.
handshakeReadKryo = KryoReader(maxMessageSize)
handshakeWriteKryo = KryoWriter(maxMessageSize)
serialization.newHandshakeKryo(handshakeReadKryo)
serialization.newHandshakeKryo(handshakeWriteKryo)
handshakeSendIdleStrategy = config.sendIdleStrategy
}
/**
* NOTE: this **MUST** stay on the same co-routine that calls "send". This cannot be re-dispatched onto a different coroutine!
* CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
* Server -> will be network polling thread
* Client -> will be thread that calls `connect()`
*
* @return true if the message was successfully sent by aeron
*/
@Suppress("DuplicatedCode")
internal fun writeMessage(publication: Publication, logInfo: String, message: HandshakeMessage): Boolean {
// The handshake sessionId IS NOT globally unique
if (logger.isTraceEnabled) {
logger.trace("[$logInfo] (${message.connectKey}) send HS: $message")
}
try {
val buffer = handshakeWriteKryo.write(message)
return aeronDriver.send(publication, buffer, logInfo, listenerManager, handshakeSendIdleStrategy)
} catch (e: Exception) {
// if the driver is closed due to a network disconnect or a remote-client termination, we also must close the connection.
if (aeronDriver.internal.mustRestartDriverOnError) {
// we had a HARD network crash/disconnect, we close the driver and then reconnect automatically
//NOTE: notifyDisconnect IS NOT CALLED!
}
else if (e is ClientException || e is ServerException) {
throw e
}
else {
val exception = newException("[$logInfo] Error serializing handshake message $message", e)
exception.cleanStackTrace(2) // 2 because we do not want to see the stack for the abstract `newException`
listenerManager.notifyError(exception)
throw exception
}
return false
} finally {
handshakeSendIdleStrategy.reset()
}
}
/**
* NOTE: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
*
* THROWS EXCEPTION IF INVALID READS!
*
* @param buffer The buffer
* @param offset The offset from the start of the buffer
* @param length The number of bytes to extract
*
* @return the message
*/
internal fun readMessage(buffer: DirectBuffer, offset: Int, length: Int): Any? {
// NOTE: This ABSOLUTELY MUST be done on the same thread! This cannot be done on a new one, because the buffer could change!
return handshakeReadKryo.read(buffer, offset, length)
}
}

View File

@ -1,120 +0,0 @@
/*
* 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.handshake
import org.agrona.collections.IntArrayList
/**
* An allocator for port numbers.
*
* The allocator accepts a base number `p` and a maximum count `n | n > 0`, and will allocate
* up to `n` numbers, in a random order, in the range `[p, p + n - 1`.
*
* @param basePort The base port
* @param numberOfPortsToAllocate The maximum number of ports that will be allocated
*
* @throws IllegalArgumentException If the port range is not valid
*/
class PortAllocator(basePort: Int, numberOfPortsToAllocate: Int) {
private val minPort: Int
private val maxPort: Int
private val portShuffleReset: Int
private var portShuffleCount: Int
private val freePorts: IntArrayList
init {
if (basePort !in 1..65535) {
throw IllegalArgumentException("Base port $basePort must be in the range [1, 65535]")
}
minPort = basePort
maxPort = Math.max(basePort+1, basePort + (numberOfPortsToAllocate - 1))
if (maxPort !in (basePort + 1)..65535) {
throw IllegalArgumentException("Uppermost port $maxPort must be in the range [$basePort, 65535]")
}
// every time we add 25% of ports back (via 'free'), reshuffle the ports
portShuffleReset = numberOfPortsToAllocate/4
portShuffleCount = portShuffleReset
freePorts = IntArrayList()
for (port in basePort..maxPort) {
freePorts.addInt(port)
}
freePorts.shuffle()
}
/**
* Allocate `count` number of ports.
*
* @param count The number of ports that will be allocated
*
* @return An array of allocated ports
*
* @throws PortAllocationException If there are fewer than `count` ports available to allocate
*/
fun allocate(count: Int): IntArray {
if (freePorts.size < count) {
throw IllegalArgumentException("Too few ports available to allocate $count ports")
}
// reshuffle the ports once we need to re-allocate a new port
if (portShuffleCount <= 0) {
portShuffleCount = portShuffleReset
freePorts.shuffle()
}
val result = IntArray(count)
for (index in 0 until count) {
val lastValue = freePorts.size - 1
val removed = freePorts.removeAt(lastValue)
result[index] = removed
}
return result
}
/**
* Frees the given ports. Has no effect if the given port is outside of the range considered by the allocator.
*
* @param ports The array of ports to free
*/
fun free(ports: IntArray) {
ports.forEach {
free(it)
}
}
/**
* Free a given port.
* <p>
* Has no effect if the given port is outside of the range considered by the allocator.
*
* @param port The port
*/
fun free(port: Int) {
if (port in minPort..maxPort) {
// add at the end (so we don't have unnecessary array resizes)
freePorts.addInt(freePorts.size, port)
portShuffleCount--
}
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2023 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.handshake
import dorkbox.network.connection.EndPoint
import io.aeron.Publication
import io.aeron.Subscription
import java.net.Inet4Address
import java.net.InetAddress
data class PubSub(
val pub: Publication,
val sub: Subscription,
val sessionIdPub: Int,
val sessionIdSub: Int,
val streamIdPub: Int,
val streamIdSub: Int,
val reliable: Boolean,
val remoteAddress: InetAddress?,
val remoteAddressString: String,
val portPub: Int,
val portSub: Int,
val tagName: String // will either be "", or will be "[tag_name]"
) {
val isIpc get() = remoteAddress == null
fun getLogInfo(extraDetails: Boolean): String {
return if (isIpc) {
val prefix = if (tagName.isNotEmpty()) {
EndPoint.IPC_NAME + " ($tagName)"
} else {
EndPoint.IPC_NAME
}
if (extraDetails) {
"$prefix sessionID: p=${sessionIdPub} s=${sessionIdSub}, streamID: p=${streamIdPub} s=${streamIdSub}, reg: p=${pub.registrationId()} s=${sub.registrationId()}"
} else {
prefix
}
} else {
var prefix = if (remoteAddress is Inet4Address) {
"IPv4 $remoteAddressString"
} else {
"IPv6 $remoteAddressString"
}
if (tagName.isNotEmpty()) {
prefix += " ($tagName)"
}
if (extraDetails) {
"$prefix sessionID: p=${sessionIdPub} s=${sessionIdSub}, streamID: p=${streamIdPub} s=${streamIdSub}, port: p=${portPub} s=${portSub}, reg: p=${pub.registrationId()} s=${sub.registrationId()}"
} else {
prefix
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,11 +15,12 @@
*/
package dorkbox.network.handshake
import dorkbox.network.connection.CryptoManagement
import dorkbox.network.exceptions.AllocationException
import dorkbox.objectPool.ObjectPool
import dorkbox.objectPool.Pool
import kotlinx.atomicfu.atomic
import java.security.SecureRandom
import org.slf4j.LoggerFactory
/**
* An allocator for random IDs, the maximum number of IDs is an unsigned short (65535).
@ -32,28 +33,33 @@ import java.security.SecureRandom
* @param min The minimum ID (inclusive)
* @param max The maximum ID (exclusive)
*/
class RandomId65kAllocator(private val min: Int = Integer.MIN_VALUE, max: Int = Integer.MAX_VALUE) {
class RandomId65kAllocator(private val min: Int, max: Int) {
constructor(size: Int): this(1, size + 1)
companion object {
private val logger = LoggerFactory.getLogger("RandomId65k")
}
private val cache: Pool<Int>
private val maxAssignments: Int
private val assigned = atomic(0)
init {
// IllegalArgumentException
require(max >= min) {
"Maximum value $max must be >= minimum value $min"
}
require(max >= min) { "Maximum value $max must be >= minimum value $min" }
maxAssignments = (max - min).coerceIn(1, Short.MAX_VALUE * 2)
val max65k = Short.MAX_VALUE * 2
maxAssignments = (max - min).coerceIn(1, max65k)
// create a shuffled list of ID's. This operation is ONLY performed ONE TIME per endpoint!
// Boxing the Ints here is OK, because they are boxed in the cache as well (so it doesn't matter).
val ids = ArrayList<Int>(maxAssignments)
for (id in min..(min + maxAssignments - 1)) {
for (id in min until min + maxAssignments) {
ids.add(id)
}
ids.shuffle(SecureRandom())
ids.shuffle(CryptoManagement.secureRandom)
// populate the array of randomly assigned ID's.
cache = ObjectPool.blocking(ids)
@ -71,8 +77,12 @@ class RandomId65kAllocator(private val min: Int = Integer.MIN_VALUE, max: Int =
throw AllocationException("No IDs left to allocate")
}
assigned.getAndIncrement()
return cache.take()
val count = assigned.incrementAndGet()
val id = cache.take()
if (logger.isTraceEnabled) {
logger.trace("Allocating $id (total $count)")
}
return id
}
/**
@ -83,13 +93,16 @@ class RandomId65kAllocator(private val min: Int = Integer.MIN_VALUE, max: Int =
fun free(id: Int) {
val assigned = assigned.decrementAndGet()
if (assigned < 0) {
throw AllocationException("Unequal allocate/free method calls.")
throw AllocationException("Unequal allocate/free method calls attempting to free [$id] (too many 'free' calls).")
}
if (logger.isTraceEnabled) {
logger.trace("Freeing $id")
}
cache.put(id)
}
fun isEmpty(): Boolean {
return assigned.value == 0
fun counts(): Int {
return assigned.value
}
override fun toString(): String {

View File

@ -0,0 +1,181 @@
/*
* Copyright 2023 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.handshake
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.AeronDriver.Companion.uri
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.IpInfo
import io.aeron.CommonContext
import java.net.Inet4Address
import java.net.InetAddress
/**
* Set up the subscription + publication channels back to the client
*
* Note: this class is NOT closed the traditional way! It's pub/sub objects are used by the connection (which is where they are closed)
*
* This represents the connection PAIR between a server<->client
*/
internal class ServerConnectionDriver(val pubSub: PubSub) {
companion object {
fun build(isIpc: Boolean,
aeronDriver: AeronDriver,
sessionIdPub: Int, sessionIdSub: Int,
streamIdPub: Int, streamIdSub: Int,
ipInfo: IpInfo,
remoteAddress: InetAddress?,
remoteAddressString: String,
portPubMdc: Int, portPub: Int, portSub: Int,
reliable: Boolean,
tagName: String,
logInfo: String): ServerConnectionDriver {
val pubSub: PubSub
if (isIpc) {
pubSub = buildIPC(
aeronDriver = aeronDriver,
sessionIdPub = sessionIdPub,
sessionIdSub = sessionIdSub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
reliable = reliable,
tagName = tagName,
logInfo = logInfo
)
} else {
pubSub = buildUdp(
aeronDriver = aeronDriver,
ipInfo = ipInfo,
sessionIdPub = sessionIdPub,
sessionIdSub = sessionIdSub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
remoteAddress = remoteAddress!!,
remoteAddressString = remoteAddressString,
portPubMdc = portPubMdc,
portPub = portPub,
portSub = portSub,
reliable = reliable,
tagName = tagName,
logInfo = logInfo
)
}
return ServerConnectionDriver(pubSub)
}
private fun buildIPC(
aeronDriver: AeronDriver,
sessionIdPub: Int, sessionIdSub: Int,
streamIdPub: Int, streamIdSub: Int,
reliable: Boolean,
tagName: String,
logInfo: String
): PubSub {
// on close, the publication CAN linger (in case a client goes away, and then comes back)
// AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param)
// create a new publication for the connection (since the handshake ALWAYS closes the current publication)
val publicationUri = uri(CommonContext.IPC_MEDIA, sessionIdPub, reliable)
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, true)
// Create a subscription at the given address and port, using the given stream ID.
val subscriptionUri = uri(CommonContext.IPC_MEDIA, sessionIdSub, reliable)
val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, true)
return PubSub(
pub = publication,
sub = subscription,
sessionIdPub = sessionIdPub,
sessionIdSub = sessionIdSub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
reliable = reliable,
remoteAddress = null,
remoteAddressString = EndPoint.IPC_NAME,
portPub = 0,
portSub = 0,
tagName = tagName
)
}
private fun buildUdp(
aeronDriver: AeronDriver,
ipInfo: IpInfo,
sessionIdPub: Int, sessionIdSub: Int,
streamIdPub: Int, streamIdSub: Int,
remoteAddress: InetAddress, remoteAddressString: String,
portPubMdc: Int, // this is the MDC port - used to dynamically discover the portPub value (but we manually save this info)
portPub: Int,
portSub: Int,
reliable: Boolean,
tagName: String,
logInfo: String
): PubSub {
// on close, the publication CAN linger (in case a client goes away, and then comes back)
// AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param)
// connection timeout of 0 doesn't matter. it is not used by the server
// the client address WILL BE either IPv4 or IPv6
val isRemoteIpv4 = remoteAddress is Inet4Address
// create a new publication for the connection (since the handshake ALWAYS closes the current publication)
// we explicitly have the publisher "connect to itself", because we are using MDC to work around NAT
// A control endpoint for the subscriptions will cause a periodic service management "heartbeat" to be sent to the
// remote endpoint publication, which permits the remote publication to send us data, thereby getting us around NAT
val publicationUri = uri(CommonContext.UDP_MEDIA, sessionIdPub, reliable)
.controlEndpoint(ipInfo.getAeronPubAddress(isRemoteIpv4) + ":" + portPubMdc) // this is the control port! (listens to status messages and NAK from client)
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be long running or re-entrant with the client.
val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, false)
// if we are IPv6 WILDCARD -- then our subscription must ALSO be IPv6, even if our connection is via IPv4
// Create a subscription at the given address and port, using the given stream ID.
val subscriptionUri = uri(CommonContext.UDP_MEDIA, sessionIdSub, reliable)
.endpoint(ipInfo.formattedListenAddressString + ":" + portSub)
val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, false)
return PubSub(
pub = publication,
sub = subscription,
sessionIdPub = sessionIdPub,
sessionIdSub = sessionIdSub,
streamIdPub = streamIdPub,
streamIdSub = streamIdSub,
reliable = reliable,
remoteAddress = remoteAddress,
remoteAddressString = remoteAddressString,
portPub = portPub,
portSub = portSub,
tagName = tagName
)
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,24 +18,20 @@ package dorkbox.network.handshake
import dorkbox.network.Server
import dorkbox.network.ServerConfiguration
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.mediaDriver.MediaDriverConnectInfo
import dorkbox.network.aeron.mediaDriver.MediaDriverConnection
import dorkbox.network.aeron.mediaDriver.ServerIpcDriver
import dorkbox.network.aeron.mediaDriver.UdpMediaDriverPairedConnection
import dorkbox.network.connection.Connection
import dorkbox.network.connection.ConnectionParams
import dorkbox.network.connection.ListenerManager
import dorkbox.network.connection.PublicKeyValidationState
import dorkbox.network.aeron.AeronDriver.Companion.sessionIdAllocator
import dorkbox.network.aeron.AeronDriver.Companion.streamIdAllocator
import dorkbox.network.connection.*
import dorkbox.network.exceptions.AllocationException
import dorkbox.network.exceptions.ServerHandshakeException
import dorkbox.network.exceptions.ServerTimedoutException
import dorkbox.network.exceptions.TransmitException
import io.aeron.Publication
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mu.KLogger
import net.jodah.expiringmap.ExpirationPolicy
import net.jodah.expiringmap.ExpiringMap
import org.slf4j.Logger
import java.net.Inet4Address
import java.net.InetAddress
import java.util.*
import java.util.concurrent.*
@ -46,45 +42,62 @@ import java.util.concurrent.*
*/
@Suppress("DuplicatedCode", "JoinDeclarationAndAssignment")
internal class ServerHandshake<CONNECTION : Connection>(
private val logger: KLogger,
private val config: ServerConfiguration,
private val listenerManager: ListenerManager<CONNECTION>,
aeronDriver: AeronDriver
private val aeronDriver: AeronDriver,
private val eventDispatch: EventDispatcher
) {
// note: the expire time here is a LITTLE longer than the expire time in the client, this way we can adjust for network lag if it's close
private val pendingConnections = ExpiringMap.builder()
// we MUST include the publication linger timeout, otherwise we might encounter problems that are NOT REALLY problems
.expiration(TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong() * 2) + aeronDriver.getLingerNs(), TimeUnit.NANOSECONDS)
.apply {
// connections are extremely difficult to diagnose when the connection timeout is short
val timeUnit = if (EndPoint.DEBUG_CONNECTIONS) { TimeUnit.HOURS } else { TimeUnit.NANOSECONDS }
// we MUST include the publication linger timeout, otherwise we might encounter problems that are NOT REALLY problems
this.expiration(TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong() * 2) + aeronDriver.lingerNs(), timeUnit)
}
.expirationPolicy(ExpirationPolicy.CREATED)
.expirationListener<Long, CONNECTION> { clientConnectKey, connection ->
// this blocks until it fully runs (which is ok. this is fast)
logger.error { "[${clientConnectKey} Connection (${connection.id}) Timed out waiting for registration response from client" }
listenerManager.notifyError(ServerTimedoutException("[${clientConnectKey} Connection (${connection.id}) Timed out waiting for registration response from client"))
connection.close()
}
.build<Long, CONNECTION>()
private val connectionsPerIpCounts = ConnectionCounts()
internal val connectionsPerIpCounts = ConnectionCounts()
// guarantee that session/stream ID's will ALWAYS be unique! (there can NEVER be a collision!)
private val sessionIdAllocator = RandomId65kAllocator(AeronDriver.RESERVED_SESSION_ID_LOW, AeronDriver.RESERVED_SESSION_ID_HIGH)
private val streamIdAllocator = RandomId65kAllocator(1, Integer.MAX_VALUE)
/**
* how long does the initial handshake take to connect
*/
internal var handshakeTimeoutNs: Long
init {
// we MUST include the publication linger timeout, otherwise we might encounter problems that are NOT REALLY problems
var handshakeTimeoutNs = TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong()) + aeronDriver.publicationConnectionTimeoutNs() + aeronDriver.lingerNs()
if (EndPoint.DEBUG_CONNECTIONS) {
// connections are extremely difficult to diagnose when the connection timeout is short
handshakeTimeoutNs = TimeUnit.HOURS.toNanos(1)
}
this.handshakeTimeoutNs = handshakeTimeoutNs
}
/**
* @return true if we should continue parsing the incoming message, false if we should abort (as we are DONE processing data)
*/
// note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD. ONLY RESPONSES ARE ON ACTION DISPATCH!
private fun validateMessageTypeAndDoPending(
fun validateMessageTypeAndDoPending(
server: Server<CONNECTION>,
actionDispatch: CoroutineScope,
handshaker: Handshaker<CONNECTION>,
handshakePublication: Publication,
message: HandshakeMessage,
connectionString: String,
aeronLogInfo: String,
logger: KLogger
logInfo: String,
logger: Logger
): Boolean {
// check to see if this sessionId is ALREADY in use by another connection!
// this can happen if there are multiple connections from the SAME ip address (ie: localhost)
if (message.state == HandshakeMessage.HELLO) {
@ -92,15 +105,17 @@ internal class ServerHandshake<CONNECTION : Connection>(
val existingConnection = pendingConnections[message.connectKey]
if (existingConnection != null) {
val existingAeronLogInfo = "${existingConnection.id}/${existingConnection.streamId}"
// Server is the "source", client mirrors the server
// WHOOPS! tell the client that it needs to retry, since a DIFFERENT client has a handshake in progress with the same sessionId
logger.error { "[$existingAeronLogInfo - ${message.connectKey}] Connection from $connectionString had an in-use session ID! Telling client to retry." }
listenerManager.notifyError(ServerHandshakeException("[$existingConnection] (${message.connectKey}) Connection had an in-use session ID! Telling client to retry."))
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo, HandshakeMessage.retry("Handshake already in progress for sessionID!"))
handshaker.writeMessage(handshakePublication,
logInfo,
HandshakeMessage.retry("Handshake already in progress for sessionID!"))
} catch (e: Error) {
logger.error(e) { "[$aeronLogInfo - $existingAeronLogInfo] Handshake error!" }
listenerManager.notifyError(ServerHandshakeException("[$existingConnection] Handshake error", e))
}
return false
}
@ -108,48 +123,47 @@ internal class ServerHandshake<CONNECTION : Connection>(
// check to see if this is a pending connection
if (message.state == HandshakeMessage.DONE) {
val existingConnection = pendingConnections.remove(message.connectKey)
if (existingConnection == null) {
logger.error { "[$aeronLogInfo - ${message.connectKey}] Error! Pending connection from client $connectionString was null, and cannot complete handshake!" }
val newConnection = pendingConnections.remove(message.connectKey)
if (newConnection == null) {
listenerManager.notifyError(ServerHandshakeException("[?????] (${message.connectKey}) Error! Pending connection from client was null, and cannot complete handshake!"))
return true
} else {
val existingAeronLogInfo = "${existingConnection.id}/${existingConnection.streamId}"
logger.debug { "[$aeronLogInfo - $existingAeronLogInfo - ${message.connectKey}] Connection from $connectionString done with handshake." }
// called on connection.close()
existingConnection.closeAction = {
// clean up the resources associated with this connection when it's closed
logger.debug { "[$existingAeronLogInfo] freeing resources" }
existingConnection.cleanup(connectionsPerIpCounts, sessionIdAllocator, streamIdAllocator)
// this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback
actionDispatch.launch {
existingConnection.doNotifyDisconnect()
}
}
// before we finish creating the connection, we initialize it (in case there needs to be logic that happens-before `onConnect` calls occur
listenerManager.notifyInit(existingConnection)
// this enables the connection to start polling for messages
server.addConnection(existingConnection)
// now tell the client we are done
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.doneToClient(message.connectKey))
// 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.notifyConnect(existingConnection)
}
} catch (e: Exception) {
logger.error(e) { "$aeronLogInfo - $existingAeronLogInfo - Handshake error!" }
}
return false
}
val connectionType = if (newConnection.enableBufferedMessages) {
"Buffered connection"
} else {
"Connection"
}
// Server is the "source", client mirrors the server
if (logger.isTraceEnabled) {
logger.trace("[${newConnection}] (${message.connectKey}) $connectionType (${newConnection.id}) done with handshake.")
} else if (logger.isDebugEnabled) {
logger.debug("[${newConnection}] $connectionType (${newConnection.id}) done with handshake.")
}
newConnection.setImage()
// before we finish creating the connection, we initialize it (in case there needs to be logic that happens-before `onConnect` calls
listenerManager.notifyInit(newConnection)
// this enables the connection to start polling for messages
server.addConnection(newConnection)
// now tell the client we are done
try {
handshaker.writeMessage(handshakePublication,
logInfo,
HandshakeMessage.doneToClient(message.connectKey))
listenerManager.notifyConnect(newConnection)
newConnection.sendBufferedMessages()
} catch (e: Exception) {
listenerManager.notifyError(newConnection, TransmitException("[$newConnection] Handshake error", e))
}
return false
}
return true
@ -161,24 +175,23 @@ internal class ServerHandshake<CONNECTION : Connection>(
// note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
private fun validateUdpConnectionInfo(
server: Server<CONNECTION>,
handshaker: Handshaker<CONNECTION>,
handshakePublication: Publication,
config: ServerConfiguration,
clientAddressString: String,
clientAddress: InetAddress,
aeronLogInfo: String,
logger: KLogger
logInfo: String
): Boolean {
try {
// VALIDATE:: Check to see if there are already too many clients connected.
if (server.connections.connectionCount() >= config.maxClientCount) {
logger.error("[$aeronLogInfo] Connection from $clientAddressString not allowed! Server is full. Max allowed is ${config.maxClientCount}")
if (server.connections.size() >= config.maxClientCount) {
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Server is full. Max allowed is ${config.maxClientCount}"))
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.error("Server is full"))
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Server is full"))
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Handshake error!" }
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return false
}
@ -186,29 +199,29 @@ internal class ServerHandshake<CONNECTION : Connection>(
// VALIDATE:: we are now connected to the client and are going to create a new connection.
val currentCountForIp = connectionsPerIpCounts.get(clientAddress)
if (currentCountForIp >= config.maxConnectionsPerIpAddress) {
if (config.maxConnectionsPerIpAddress in 1..currentCountForIp) {
// decrement it now, since we aren't going to permit this connection (take the extra decrement hit on failure, instead of always)
connectionsPerIpCounts.decrement(clientAddress, currentCountForIp)
logger.error { "[$aeronLogInfo] Too many connections for IP address $clientAddressString. Max allowed is ${config.maxConnectionsPerIpAddress}" }
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Too many connections for IP address. Max allowed is ${config.maxConnectionsPerIpAddress}"))
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.error("Too many connections for IP address"))
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Too many connections for IP address"))
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Handshake error!" }
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return false
}
connectionsPerIpCounts.increment(clientAddress, currentCountForIp)
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Could not validate client message" }
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Handshake error, Could not validate client message", e))
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.error("Invalid connection"))
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Invalid connection"))
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Handshake error!" }
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
}
@ -217,34 +230,28 @@ internal class ServerHandshake<CONNECTION : Connection>(
/**
* @return true if the handshake poller is to close the publication, false will keep the publication (as we are DONE processing data)
* NOTE: This must not be called on the main thread because it is blocking!
*
* @return true if the connection was SUCCESS. False if the handshake poller should immediately close the publication
*/
fun processIpcHandshakeMessageServer(
server: Server<CONNECTION>,
handshakePublication: Publication,
message: HandshakeMessage,
handshaker: Handshaker<CONNECTION>,
aeronDriver: AeronDriver,
aeronLogInfo: String,
connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION,
logger: KLogger
) {
val connectionString = "IPC"
if (!validateMessageTypeAndDoPending(
server,
server.actionDispatch,
handshakePublication,
message,
connectionString,
aeronLogInfo,
logger
)) {
return
}
handshakePublication: Publication,
publicKey: ByteArray,
message: HandshakeMessage,
logInfo: String,
logger: Logger
): Boolean {
val serialization = config.serialization
val clientTagName = message.tag
if (clientTagName.length > 32) {
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Invalid tag name."))
return false
}
/////
/////
///// DONE WITH VALIDATION
@ -253,87 +260,134 @@ internal class ServerHandshake<CONNECTION : Connection>(
// allocate session/stream id's
val connectionSessionId: Int
val connectionSessionIdPub: Int
try {
connectionSessionId = sessionIdAllocator.allocate()
connectionSessionIdPub = sessionIdAllocator.allocate()
} catch (e: AllocationException) {
logger.error { "[$aeronLogInfo] Connection from $connectionString not allowed! Unable to allocate a session ID for the client connection!" }
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a session pub ID for the client connection!", e))
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.error("Connection error!"))
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Handshake error!" }
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return
return false
}
val connectionStreamPubId: Int
val connectionSessionIdSub: Int
try {
connectionStreamPubId = streamIdAllocator.allocate()
connectionSessionIdSub = sessionIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
sessionIdAllocator.free(connectionSessionId)
sessionIdAllocator.free(connectionSessionIdPub)
logger.error { "[$aeronLogInfo] Connection from $connectionString not allowed! Unable to allocate a stream ID for the client connection!" }
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a session sub ID for the client connection!", e))
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.error("Connection error!"))
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Handshake error!" }
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return
return false
}
val connectionStreamSubId: Int
val connectionStreamIdPub: Int
try {
connectionStreamSubId = streamIdAllocator.allocate()
connectionStreamIdPub = streamIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
sessionIdAllocator.free(connectionSessionId)
sessionIdAllocator.free(connectionStreamPubId)
sessionIdAllocator.free(connectionSessionIdPub)
sessionIdAllocator.free(connectionSessionIdSub)
logger.error { "[$aeronLogInfo] Connection from $connectionString not allowed! Unable to allocate a stream ID for the client connection!" }
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a stream publication ID for the client connection!", e))
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.error("Connection error!"))
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Handshake error!" }
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return
return false
}
val connectionStreamIdSub: Int
try {
connectionStreamIdSub = streamIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
sessionIdAllocator.free(connectionSessionIdPub)
sessionIdAllocator.free(connectionSessionIdSub)
streamIdAllocator.free(connectionStreamIdPub)
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a stream subscription ID for the client connection!", e))
try {
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return false
}
// create a new connection. The session ID is encrypted.
var newConnection: CONNECTION? = null
try {
// Create a subscription at the given address and port, using the given stream ID.
val driver = ServerIpcDriver(streamId = connectionStreamSubId,
sessionId = connectionSessionId)
driver.build(aeronDriver, logger)
// Create a pub/sub at the given address and port, using the given stream ID.
// NOTE: This must not be called on the main thread because it is blocking!
val newConnectionDriver = ServerConnectionDriver.build(
aeronDriver = aeronDriver,
ipInfo = server.ipInfo,
isIpc = true,
tagName = clientTagName,
logInfo = EndPoint.IPC_NAME,
// create a new publication for the connection (since the handshake ALWAYS closes the current publication)
val publicationUri = MediaDriverConnection.uri("ipc", handshakePublication.sessionId())
val clientPublication = aeronDriver.addPublication(publicationUri, message.subscriptionPort)
val clientConnection = MediaDriverConnectInfo(
publication = clientPublication,
subscription = driver.subscription,
subscriptionPort = driver.streamId,
publicationPort = message.subscriptionPort,
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"
remoteAddressString = "",
sessionIdPub = connectionSessionIdPub,
sessionIdSub = connectionSessionIdSub,
streamIdPub = connectionStreamIdPub,
streamIdSub = connectionStreamIdSub,
portPubMdc = 0,
portPub = 0,
portSub = 0,
reliable = true
)
logger.info { "[$aeronLogInfo] Creating new IPC connection from $driver" }
val enableBufferedMessagesForConnection = listenerManager.notifyEnableBufferedMessages(null, clientTagName)
val connectionType = if (enableBufferedMessagesForConnection) {
"buffered connection"
} else {
"connection"
}
val connectionTypeCaps = connectionType.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
val logInfo = newConnectionDriver.pubSub.getLogInfo(logger.isDebugEnabled)
if (logger.isDebugEnabled) {
logger.debug("Creating new $connectionType to $logInfo")
} else {
logger.info("Creating new $connectionType to $logInfo")
}
newConnection = server.newConnection(ConnectionParams(
publicKey = publicKey,
endPoint = server,
connectionInfo = newConnectionDriver.pubSub,
publicKeyValidation = PublicKeyValidationState.VALID,
enableBufferedMessages = enableBufferedMessagesForConnection,
cryptoKey = CryptoManagement.NOCRYPT // we don't use encryption for IPC connections
))
server.bufferedManager.onConnect(newConnection)
val connection = connectionFunc(ConnectionParams(server, clientConnection, PublicKeyValidationState.VALID))
// VALIDATE:: are we allowed to connect to this server (now that we have the initial server information)
// NOTE: all IPC client connections are, by default, always allowed to connect, because they are running on the same machine
@ -344,82 +398,112 @@ internal class ServerHandshake<CONNECTION : Connection>(
///////////////
// The one-time pad is used to encrypt the session ID, so that ONLY the correct client knows what it is!
val successMessage = HandshakeMessage.helloAckIpcToClient(message.connectKey)
// if necessary, we also send the kryo RMI id's that are registered as RMI on this endpoint, but maybe not on the other endpoint
// Also send the RMI registration data to the client (so the client doesn't register anything)
// now create the encrypted payload, using ECDH
val cryptOutput = server.crypto.cryptOutput
cryptOutput.reset()
cryptOutput.writeInt(connectionSessionId)
cryptOutput.writeInt(connectionStreamSubId)
val regDetails = serialization.getKryoRegistrationDetails()
cryptOutput.writeInt(regDetails.size)
cryptOutput.writeBytes(regDetails)
successMessage.registrationData = cryptOutput.toBytes()
// now create the encrypted payload, using no crypto
successMessage.registrationData = server.crypto.nocrypt(
sessionIdPub = connectionSessionIdPub,
sessionIdSub = connectionSessionIdSub,
streamIdPub = connectionStreamIdPub,
streamIdSub = connectionStreamIdSub,
sessionTimeout = config.bufferedConnectionTimeoutSeconds,
bufferedMessages = enableBufferedMessagesForConnection,
kryoRegDetails = serialization.getKryoRegistrationDetails()
)
successMessage.publicKey = server.crypto.publicKeyBytes
// before we notify connect, we have to wait for the client to tell us that they can receive data
pendingConnections[message.connectKey] = connection
pendingConnections[message.connectKey] = newConnection
if (logger.isTraceEnabled) {
logger.trace("[$logInfo] (${message.connectKey}) $connectionType (${newConnection.id}) responding to handshake hello.")
} else if (logger.isDebugEnabled) {
logger.debug("[$logInfo] $connectionTypeCaps (${newConnection.id}) responding to handshake hello.")
}
// this tells the client all the info to connect.
server.writeHandshakeMessage(handshakePublication, aeronLogInfo, successMessage) // exception is already caught!
handshaker.writeMessage(handshakePublication, logInfo, successMessage) // exception is already caught!
} catch (e: Exception) {
// have to unwind actions!
sessionIdAllocator.free(connectionSessionId)
streamIdAllocator.free(connectionStreamPubId)
sessionIdAllocator.free(connectionSessionIdPub)
sessionIdAllocator.free(connectionSessionIdSub)
streamIdAllocator.free(connectionStreamIdSub)
streamIdAllocator.free(connectionStreamIdPub)
logger.error(e) { "[$aeronLogInfo] Connection handshake from $connectionString crashed! Message $message" }
listenerManager.notifyError(ServerHandshakeException("[$logInfo] (${message.connectKey}) Connection (${newConnection?.id}) handshake crashed! Message $message", e))
return false
}
return true
}
/**
* note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
* @return true if the handshake poller is to close the publication, false will keep the publication
* NOTE: This must not be called on the main thread because it is blocking!
*
* @return true if the connection was SUCCESS. False if the handshake poller should immediately close the publication
*/
fun processUdpHandshakeMessageServer(
server: Server<CONNECTION>,
handshaker: Handshaker<CONNECTION>,
handshakePublication: Publication,
publicKey: ByteArray,
clientAddress: InetAddress,
clientAddressString: String,
portSub: Int,
portPub: Int,
mdcPortPub: Int,
isReliable: Boolean,
message: HandshakeMessage,
aeronDriver: AeronDriver,
aeronLogInfo: String,
isIpv6Wildcard: Boolean,
connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION,
logger: KLogger
) {
// Manage the Handshake state
if (!validateMessageTypeAndDoPending(
server, server.actionDispatch, handshakePublication, message,
clientAddressString, aeronLogInfo, logger))
{
return
}
logInfo: String,
logger: Logger
): Boolean {
val serialization = config.serialization
// UDP ONLY
val clientPublicKeyBytes = message.publicKey
val validateRemoteAddress: PublicKeyValidationState
val serialization = config.serialization
// VALIDATE:: check to see if the remote connection's public key has changed!
validateRemoteAddress = server.crypto.validateRemoteAddress(clientAddress, clientAddressString, clientPublicKeyBytes)
if (validateRemoteAddress == PublicKeyValidationState.INVALID) {
logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Public key mismatch." }
return
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Public key mismatch."))
return false
}
clientPublicKeyBytes!!
if (!clientAddress.isLoopbackAddress &&
!validateUdpConnectionInfo(server, handshakePublication, config, clientAddressString, clientAddress, aeronLogInfo, logger)) {
val isSelfMachine = clientAddress.isLoopbackAddress || clientAddress == EndPoint.lanAddress
if (!isSelfMachine &&
!validateUdpConnectionInfo(server, handshaker, handshakePublication, config, clientAddress, logInfo)) {
// we do not want to limit the loopback addresses!
return
return false
}
val clientTagName = message.tag
if (clientTagName.length > 32) {
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Invalid tag name."))
return false
}
// VALIDATE:: are we allowed to connect to this server (now that we have the initial server information)
val permitConnection = listenerManager.notifyFilter(clientAddress, clientTagName)
if (!permitConnection) {
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection was not permitted!"))
try {
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Connection was not permitted!"))
} catch (e: Exception) {
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return false
}
@ -431,121 +515,152 @@ internal class ServerHandshake<CONNECTION : Connection>(
// allocate session/stream id's
val connectionSessionId: Int
val connectionSessionIdPub: Int
try {
connectionSessionId = sessionIdAllocator.allocate()
connectionSessionIdPub = sessionIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
connectionsPerIpCounts.decrementSlow(clientAddress)
logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Unable to allocate a session ID for the client connection!" }
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a session ID for the client connection!"))
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.error("Connection error!"))
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Handshake error!" }
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return
return false
}
val connectionStreamId: Int
val connectionSessionIdSub: Int
try {
connectionStreamId = streamIdAllocator.allocate()
connectionSessionIdSub = sessionIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
connectionsPerIpCounts.decrementSlow(clientAddress)
sessionIdAllocator.free(connectionSessionId)
sessionIdAllocator.free(connectionSessionIdPub)
logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Unable to allocate a stream ID for the client connection!" }
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a session ID for the client connection!"))
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.error("Connection error!"))
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Handshake error!" }
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return
return false
}
// the pub/sub do not necessarily have to be the same. They can be ANY port
val publicationPort = message.subscriptionPort
val subscriptionPort = config.port
val connectionStreamIdPub: Int
try {
connectionStreamIdPub = streamIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
connectionsPerIpCounts.decrementSlow(clientAddress)
sessionIdAllocator.free(connectionSessionIdPub)
sessionIdAllocator.free(connectionSessionIdSub)
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a stream ID for the client connection!"))
try {
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return false
}
val connectionStreamIdSub: Int
try {
connectionStreamIdSub = streamIdAllocator.allocate()
} catch (e: AllocationException) {
// have to unwind actions!
connectionsPerIpCounts.decrementSlow(clientAddress)
sessionIdAllocator.free(connectionSessionIdPub)
sessionIdAllocator.free(connectionSessionIdSub)
streamIdAllocator.free(connectionStreamIdPub)
listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a stream ID for the client connection!"))
try {
handshaker.writeMessage(handshakePublication, logInfo,
HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e))
}
return false
}
val logType = if (clientAddress is Inet4Address) {
"IPv4"
} else {
"IPv6"
}
// create a new connection. The session ID is encrypted.
var connection: CONNECTION? = null
var newConnection: CONNECTION? = null
try {
// connection timeout of 0 doesn't matter. it is not used by the server
// the client address WILL BE either IPv4 or IPv6
val listenAddress = if (clientAddress is Inet4Address && !isIpv6Wildcard) {
server.listenIPv4Address!!
} else {
// wildcard is SPECIAL, in that if we bind wildcard, it will ALSO bind to IPv4, so we can't bind both!
server.listenIPv6Address!!
}
// Create a pub/sub at the given address and port, using the given stream ID.
// NOTE: This must not be called on the main thread because it is blocking!
val newConnectionDriver = ServerConnectionDriver.build(
ipInfo = server.ipInfo,
aeronDriver = aeronDriver,
isIpc = false,
logInfo = logType,
// create a new publication for the connection (since the handshake ALWAYS closes the current publication)
val publicationUri = MediaDriverConnection.uriEndpoint("udp", message.sessionId, isReliable, clientAddress, clientAddressString, message.subscriptionPort)
val clientPublication = aeronDriver.addPublication(publicationUri, message.streamId)
val driver = UdpMediaDriverPairedConnection(
listenAddress,
clientAddress,
clientAddressString,
publicationPort,
subscriptionPort,
connectionStreamId,
connectionSessionId,
0,
isReliable,
clientPublication
)
driver.build(aeronDriver, logger)
logger.info { "[$aeronLogInfo] Creating new connection from $driver" }
val clientConnection = MediaDriverConnectInfo(
publication = driver.publication,
subscription = driver.subscription,
subscriptionPort = driver.port,
publicationPort = publicationPort,
streamId = driver.streamId,
sessionId = driver.sessionId,
isReliable = driver.isReliable,
remoteAddress = clientAddress,
remoteAddressString = clientAddressString
remoteAddressString = clientAddressString,
sessionIdPub = connectionSessionIdPub,
sessionIdSub = connectionSessionIdSub,
streamIdPub = connectionStreamIdPub,
streamIdSub = connectionStreamIdSub,
portPubMdc = mdcPortPub,
portPub = portPub,
portSub = portSub,
tagName = clientTagName,
reliable = isReliable
)
connection = connectionFunc(ConnectionParams(server, clientConnection, validateRemoteAddress))
val cryptoSecretKey = server.crypto.generateAesKey(clientPublicKeyBytes, clientPublicKeyBytes, server.crypto.publicKeyBytes)
// VALIDATE:: are we allowed to connect to this server (now that we have the initial server information)
val permitConnection = listenerManager.notifyFilter(connection)
if (!permitConnection) {
// have to unwind actions!
connectionsPerIpCounts.decrementSlow(clientAddress)
sessionIdAllocator.free(connectionSessionId)
streamIdAllocator.free(connectionStreamId)
connection.close()
logger.error { "[$aeronLogInfo] Connection $clientAddressString was not permitted!" }
try {
server.writeHandshakeMessage(handshakePublication, aeronLogInfo,
HandshakeMessage.error("Connection was not permitted!"))
} catch (e: Exception) {
logger.error(e) { "[$aeronLogInfo] Handshake error!" }
}
return
val enableBufferedMessagesForConnection = listenerManager.notifyEnableBufferedMessages(clientAddress, clientTagName)
val connectionType = if (enableBufferedMessagesForConnection) {
"buffered connection"
} else {
"connection"
}
val connectionTypeCaps = connectionType.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
val logInfo = newConnectionDriver.pubSub.getLogInfo(logger.isDebugEnabled)
if (logger.isDebugEnabled) {
logger.debug("Creating new $connectionType to $logInfo")
} else {
logger.info("Creating new $connectionType to $logInfo")
}
newConnection = server.newConnection(ConnectionParams(
publicKey = publicKey,
endPoint = server,
connectionInfo = newConnectionDriver.pubSub,
publicKeyValidation = validateRemoteAddress,
enableBufferedMessages = enableBufferedMessagesForConnection,
cryptoKey = cryptoSecretKey
))
server.bufferedManager.onConnect(newConnection)
///////////////
/// HANDSHAKE
///////////////
// The one-time pad is used to encrypt the session ID, so that ONLY the correct client knows what it is!
val successMessage = HandshakeMessage.helloAckToClient(message.connectKey)
@ -553,29 +668,43 @@ internal class ServerHandshake<CONNECTION : Connection>(
// Also send the RMI registration data to the client (so the client doesn't register anything)
// now create the encrypted payload, using ECDH
successMessage.registrationData = server.crypto.encrypt(clientPublicKeyBytes!!,
subscriptionPort,
connectionSessionId,
connectionStreamId,
serialization.getKryoRegistrationDetails())
successMessage.registrationData = server.crypto.encrypt(
cryptoSecretKey = cryptoSecretKey,
sessionIdPub = connectionSessionIdPub,
sessionIdSub = connectionSessionIdSub,
streamIdPub = connectionStreamIdPub,
streamIdSub = connectionStreamIdSub,
sessionTimeout = config.bufferedConnectionTimeoutSeconds,
bufferedMessages = enableBufferedMessagesForConnection,
kryoRegDetails = serialization.getKryoRegistrationDetails()
)
successMessage.publicKey = server.crypto.publicKeyBytes
// before we notify connect, we have to wait for the client to tell us that they can receive data
pendingConnections[message.connectKey] = connection
pendingConnections[message.connectKey] = newConnection
logger.debug { "[$aeronLogInfo - ${message.connectKey}] Connection (${connection.streamId}/${connection.id}) responding to handshake hello." }
if (logger.isTraceEnabled) {
logger.trace("[$logInfo] $connectionTypeCaps (${newConnection.id}) responding to handshake hello.")
} else if (logger.isDebugEnabled) {
logger.debug("[$logInfo] $connectionTypeCaps (${newConnection.id}) responding to handshake hello.")
}
// this tells the client all the info to connect.
server.writeHandshakeMessage(handshakePublication, aeronLogInfo, successMessage) // exception is already caught
handshaker.writeMessage(handshakePublication, logInfo, successMessage) // exception is already caught
} catch (e: Exception) {
// have to unwind actions!
connectionsPerIpCounts.decrementSlow(clientAddress)
sessionIdAllocator.free(connectionSessionId)
streamIdAllocator.free(connectionStreamId)
sessionIdAllocator.free(connectionSessionIdPub)
sessionIdAllocator.free(connectionSessionIdSub)
streamIdAllocator.free(connectionStreamIdPub)
streamIdAllocator.free(connectionStreamIdSub)
logger.error(e) { "[$aeronLogInfo - ${message.connectKey}] Connection (${connection?.streamId}/${connection?.id}) handshake from $clientAddressString crashed! Message $message" }
listenerManager.notifyError(ServerHandshakeException("[$logInfo] (${message.connectKey}) Connection (${newConnection?.id}) handshake crashed! Message $message", e))
return false
}
return true
}
/**
@ -584,14 +713,11 @@ internal class ServerHandshake<CONNECTION : Connection>(
* note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
*/
fun checkForMemoryLeaks() {
val noAllocations = connectionsPerIpCounts.isEmpty() && sessionIdAllocator.isEmpty() && streamIdAllocator.isEmpty()
val noAllocations = connectionsPerIpCounts.isEmpty()
if (!noAllocations) {
throw AllocationException("Unequal allocate/free method calls for validation. \n" +
"connectionsPerIpCounts: '$connectionsPerIpCounts' \n" +
"sessionIdAllocator: $sessionIdAllocator \n" +
"streamIdAllocator: $streamIdAllocator")
throw AllocationException("Unequal allocate/free method calls for IP validation. \n" +
"connectionsPerIpCounts: '$connectionsPerIpCounts'")
}
}
@ -601,12 +727,17 @@ internal class ServerHandshake<CONNECTION : Connection>(
* note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
*/
fun clear() {
runBlocking {
pendingConnections.forEach { (_, v) ->
v.close()
}
val connections = pendingConnections
val latch = CountDownLatch(connections.size)
pendingConnections.clear()
eventDispatch.CLOSE.launch {
connections.forEach { (_, v) ->
v.close()
latch.countDown()
}
}
latch.await(config.connectionCloseTimeoutInSeconds.toLong() * connections.size, TimeUnit.MILLISECONDS)
connections.clear()
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2023 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.handshake
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.AeronDriver.Companion.uriHandshake
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.IpInfo
import io.aeron.ChannelUriStringBuilder
import io.aeron.CommonContext
import io.aeron.Subscription
/**
* For a client, the ports specified here MUST be manually flipped because they are in the perspective of the SERVER.
* A connection timeout of 0, means to wait forever
*/
internal class ServerHandshakeDriver(
private val aeronDriver: AeronDriver,
val subscription: Subscription,
val info: String,
private val logInfo: String)
{
companion object {
fun build(
aeronDriver: AeronDriver,
isIpc: Boolean,
ipInfo: IpInfo,
port: Int,
streamIdSub: Int, sessionIdSub: Int,
logInfo: String
): ServerHandshakeDriver {
val info: String
val subscriptionUri: ChannelUriStringBuilder
if (isIpc) {
subscriptionUri = uriHandshake(CommonContext.IPC_MEDIA, true)
info = "$logInfo [$sessionIdSub|$streamIdSub]"
} else {
// are we ipv4 or ipv6 or ipv6wildcard?
subscriptionUri = uriHandshake(CommonContext.UDP_MEDIA, ipInfo.isReliable)
.endpoint(ipInfo.getAeronPubAddress(ipInfo.isIpv4) + ":" + port)
info = "$logInfo ${ipInfo.listenAddressStringPretty}:$port [$sessionIdSub|$streamIdSub] (reliable:${ipInfo.isReliable})"
}
val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, isIpc)
return ServerHandshakeDriver(aeronDriver, subscription, info, logInfo)
}
}
fun close(endPoint: EndPoint<*>) {
try {
// we might not be able to close this connection.
aeronDriver.close(subscription, logInfo)
}
catch (e: Exception) {
endPoint.listenerManager.notifyError(e)
}
}
fun unsafeClose() {
// we might not be able to close this connection.
aeronDriver.close(subscription, logInfo)
}
override fun toString(): String {
return info
}
}

View File

@ -1,22 +1,49 @@
/*
* Copyright 2024 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.
*/
@file:Suppress("MemberVisibilityCanBePrivate", "DuplicatedCode")
package dorkbox.network.handshake
import dorkbox.netUtil.IP
import dorkbox.network.Configuration
import dorkbox.network.Server
import dorkbox.network.ServerConfiguration
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.AeronDriver.Companion.uriHandshake
import dorkbox.network.aeron.AeronPoller
import dorkbox.network.aeron.mediaDriver.MediaDriverConnection
import dorkbox.network.aeron.mediaDriver.ServerIpcDriver
import dorkbox.network.aeron.mediaDriver.ServerUdpDriver
import dorkbox.network.connection.Connection
import dorkbox.network.connection.ConnectionParams
import dorkbox.network.connection.IpInfo
import dorkbox.network.exceptions.ServerException
import dorkbox.network.exceptions.ServerHandshakeException
import dorkbox.network.exceptions.ServerTimedoutException
import dorkbox.util.NamedThreadFactory
import dorkbox.util.Sys
import io.aeron.CommonContext
import io.aeron.FragmentAssembler
import io.aeron.Image
import io.aeron.Publication
import io.aeron.logbuffer.FragmentHandler
import io.aeron.logbuffer.Header
import mu.KLogger
import net.jodah.expiringmap.ExpirationPolicy
import net.jodah.expiringmap.ExpiringMap
import org.agrona.DirectBuffer
import org.slf4j.Logger
import java.net.Inet4Address
import java.util.concurrent.*
internal object ServerHandshakePollers {
fun disabled(serverInfo: String): AeronPoller {
@ -27,286 +54,699 @@ internal object ServerHandshakePollers {
}
}
private fun <CONNECTION : Connection> ipcProcessing(
logger: KLogger,
server: Server<CONNECTION>, aeronDriver: AeronDriver,
header: Header, buffer: DirectBuffer, offset: Int, length: Int,
handshake: ServerHandshake<CONNECTION>, connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION
) {
// this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe!
class IpcProc<CONNECTION : Connection>(
val logger: Logger,
val server: Server<CONNECTION>,
val driver: AeronDriver,
val handshake: ServerHandshake<CONNECTION>
): FragmentHandler {
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
// for the handshake, the sessionId IS NOT GLOBALLY UNIQUE
val sessionId = header.sessionId()
val streamId = header.streamId()
val aeronLogInfo = "$sessionId/$streamId"
private val isReliable = server.config.isReliable
private val handshaker = server.handshaker
private val handshakeTimeoutNs = handshake.handshakeTimeoutNs
private val shutdownInProgress = server.shutdownInProgress
private val shutdown = server.shutdown
val message = server.readHandshakeMessage(buffer, offset, length, header, aeronLogInfo)
// note: the expire time here is a LITTLE longer than the expire time in the client, this way we can adjust for network lag if it's close
private val publications = ExpiringMap.builder()
.apply {
this.expiration(handshakeTimeoutNs, TimeUnit.NANOSECONDS)
}
.expirationPolicy(ExpirationPolicy.CREATED)
.expirationListener<Long, Publication> { connectKey, publication ->
try {
// we might not be able to close this connection.
driver.close(publication, "Server IPC Handshake ($connectKey)")
}
catch (e: Exception) {
server.listenerManager.notifyError(e)
}
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (message !is HandshakeMessage) {
logger.error { "[$aeronLogInfo] Connection from IPC not allowed! Invalid connection request" }
} else {
// we create a NEW publication for the handshake, which connects directly to the client handshake subscription
val publicationUri = MediaDriverConnection.uri("ipc", message.sessionId)
val publication = aeronDriver.addPublication(publicationUri, message.subscriptionPort)
}
.build<Long, Publication>()
handshake.processIpcHandshakeMessageServer(
server, publication, message,
aeronDriver, aeronLogInfo,
connectionFunc, logger
)
override fun onFragment(buffer: DirectBuffer, offset: Int, length: Int, header: Header) {
// this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe!
publication.close()
}
}
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
// for the handshake, the sessionId IS NOT GLOBALLY UNIQUE
val sessionId = header.sessionId()
val streamId = header.streamId()
val image = header.context() as Image
private fun <CONNECTION : Connection> ipProcessing(
logger: KLogger,
server: Server<CONNECTION>, isReliable: Boolean, aeronDriver: AeronDriver, isIpv6Wildcard: Boolean,
header: Header, buffer: DirectBuffer, offset: Int, length: Int,
handshake: ServerHandshake<CONNECTION>, connectionFunc: (connectionParameters: ConnectionParams<CONNECTION>) -> CONNECTION
) {
// this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe!
val logInfo = "$sessionId/$streamId : IPC" // Server is the "source", client mirrors the server
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
// for the handshake, the sessionId IS NOT GLOBALLY UNIQUE
val sessionId = header.sessionId()
val streamId = header.streamId()
val aeronLogInfo = "$sessionId/$streamId"
// note: this address will ALWAYS be an IP:PORT combo OR it will be aeron:ipc (if IPC, it will be a different handler!)
val remoteIpAndPort = (header.context() as Image).sourceIdentity()
// split
val splitPoint = remoteIpAndPort.lastIndexOf(':')
val clientAddressString = remoteIpAndPort.substring(0, splitPoint)
val message = server.readHandshakeMessage(buffer, offset, length, header, aeronLogInfo)
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (message !is HandshakeMessage) {
logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Invalid connection request" }
} else {
// this should never be null, because we are feeding it a valid IP address from aeron
val clientAddress = IP.toAddress(clientAddressString)
if (clientAddress == null) {
logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Invalid IP address!" }
if (shutdownInProgress.value) {
driver.deleteLogFile(image)
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] server is shutting down. Aborting new connection attempts."))
return
}
// we create a NEW publication for the handshake, which connects directly to the client handshake subscription
val publicationUri = MediaDriverConnection.uriEndpoint("udp", message.sessionId, isReliable, clientAddress, clientAddressString, message.subscriptionPort)
val publication = aeronDriver.addPublication(publicationUri, message.streamId)
// ugh, this is verbose -- but necessary
val message = try {
val msg = handshaker.readMessage(buffer, offset, length)
handshake.processUdpHandshakeMessageServer(
server, publication, clientAddress, clientAddressString, isReliable, message,
aeronDriver, aeronLogInfo, isIpv6Wildcard,
connectionFunc, logger
)
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (msg !is HandshakeMessage) {
throw ServerHandshakeException("[$logInfo] Connection not allowed! unrecognized message: $msg")
} else if (logger.isTraceEnabled) {
logger.trace("[$logInfo] (${msg.connectKey}) received HS: $msg")
}
msg
} catch (e: Exception) {
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error de-serializing handshake message!!", e))
null
}
publication.close()
if (message == null) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
return
}
// NOTE: This MUST to happen in separates thread so that we can take as long as we need when creating publications and handshaking,
// because under load -- this will REGULARLY timeout! Under no circumstance can this happen in the main processing thread!!
server.eventDispatch.HANDSHAKE.launch {
// we have read all the data, now dispatch it.
// HandshakeMessage.HELLO
// HandshakeMessage.DONE
val messageState = message.state
val connectKey = message.connectKey
if (messageState == HandshakeMessage.HELLO) {
// we create a NEW publication for the handshake, which connects directly to the client handshake subscription
val publicationUri = uriHandshake(CommonContext.IPC_MEDIA, isReliable)
// this will always connect to the CLIENT handshake subscription!
val publication = try {
driver.addPublication(publicationUri, message.streamId, logInfo, true)
}
catch (e: Exception) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Cannot create IPC publication back to client remote process", e))
return@launch
}
try {
// we actually have to wait for it to connect before we continue
driver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause ->
ServerTimedoutException("$logInfo publication cannot connect with client in ${Sys.getTimePrettyFull(handshakeTimeoutNs)}", cause)
}
}
catch (e: Exception) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Cannot create IPC publication back to client remote process", e))
return@launch
}
try {
val success = handshake.processIpcHandshakeMessageServer(
server = server,
handshaker = handshaker,
aeronDriver = driver,
handshakePublication = publication,
publicKey = message.publicKey!!,
message = message,
logInfo = logInfo,
logger = logger
)
if (success) {
publications[connectKey] = publication
}
else {
try {
// we might not be able to close this connection.
driver.close(publication, logInfo)
}
catch (e: Exception) {
server.listenerManager.notifyError(e)
}
}
}
catch (e: Exception) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
try {
// we might not be able to close this connection.
driver.close(publication, logInfo)
}
catch (e: Exception) {
server.listenerManager.notifyError(e)
}
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error processing IPC handshake", e))
}
} else {
// HandshakeMessage.DONE
val publication = publications.remove(connectKey)
if (publication == null) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] No publication back to IPC"))
return@launch
}
try {
handshake.validateMessageTypeAndDoPending(
server = server,
handshaker = handshaker,
handshakePublication = publication,
message = message,
logInfo = logInfo,
logger = logger
)
} catch (e: Exception) {
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error processing IPC handshake", e))
}
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
try {
// we might not be able to close this connection.
driver.close(publication, logInfo)
}
catch (e: Exception) {
server.listenerManager.notifyError(e)
}
}
}
}
fun close() {
publications.forEach { (connectKey, publication) ->
AeronDriver.sessionIdAllocator.free(publication.sessionId())
try {
// we might not be able to close this connection.
driver.close(publication, "Server Handshake ($connectKey)")
}
catch (e: Exception) {
server.listenerManager.notifyError(e)
}
}
publications.clear()
}
}
class UdpProc<CONNECTION : Connection>(
val logger: Logger,
val server: Server<CONNECTION>,
val driver: AeronDriver,
val handshake: ServerHandshake<CONNECTION>,
val isReliable: Boolean
): FragmentHandler {
companion object {
init {
ExpiringMap.setThreadFactory(NamedThreadFactory("ExpiringMap-Eviction", Configuration.networkThreadGroup, true))
}
}
private val ipInfo = server.ipInfo
private val handshaker = server.handshaker
private val handshakeTimeoutNs = handshake.handshakeTimeoutNs
private val shutdownInProgress = server.shutdownInProgress
private val shutdown = server.shutdown
private val serverPortSub = server.port1
// MDC 'dynamic control mode' means that the server will to listen for status messages and NAK (from the client) on a port.
private val mdcPortPub = server.port2
// note: the expire time here is a LITTLE longer than the expire time in the client, this way we can adjust for network lag if it's close
private val publications = ExpiringMap.builder()
.apply {
this.expiration(handshakeTimeoutNs, TimeUnit.NANOSECONDS)
}
.expirationPolicy(ExpirationPolicy.CREATED)
.expirationListener<Long, Publication> { connectKey, publication ->
try {
// we might not be able to close this connection.
driver.close(publication, "Server UDP Handshake ($connectKey)")
}
catch (e: Exception) {
server.listenerManager.notifyError(e)
}
}
.build<Long, Publication>()
override fun onFragment(buffer: DirectBuffer, offset: Int, length: Int, header: Header) {
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
// for the handshake, the sessionId IS NOT GLOBALLY UNIQUE
fun <CONNECTION : Connection> ipc(
aeronDriver: AeronDriver, config: ServerConfiguration, server: Server<CONNECTION>, handshake: ServerHandshake<CONNECTION>
): AeronPoller
{
// this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe!
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
// for the handshake, the sessionId IS NOT GLOBALLY UNIQUE
val sessionId = header.sessionId()
val streamId = header.streamId()
val image = header.context() as Image
// note: this address will ALWAYS be an IP:PORT combo OR it will be aeron:ipc (if IPC, it will be a different handler!)
val remoteIpAndPort = image.sourceIdentity()
// split
val splitPoint = remoteIpAndPort.lastIndexOf(':')
var clientAddressString = remoteIpAndPort.substring(0, splitPoint)
// this should never be null, because we are feeding it a valid IP address from aeron
val clientAddress = IP.toAddress(clientAddressString)
if (clientAddress == null) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
// Server is the "source", client mirrors the server
server.listenerManager.notifyError(ServerHandshakeException("[$sessionId/$streamId] Connection from $clientAddressString not allowed! Invalid IP address!"))
return
}
val isRemoteIpv4 = clientAddress is Inet4Address
if (!isRemoteIpv4) {
// this is necessary to clean up the address when adding it to aeron, since different formats mess it up
clientAddressString = IP.toString(clientAddress)
if (ipInfo.ipType == IpInfo.Companion.IpListenType.IPv4Wildcard) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
// we DO NOT want to listen to IPv4 traffic, but we received IPv4 traffic!
server.listenerManager.notifyError(ServerHandshakeException("[$sessionId/$streamId] Connection from $clientAddressString not allowed! IPv4 connections not permitted!"))
return
}
}
val logInfo = "$sessionId/$streamId:$clientAddressString"
if (shutdownInProgress.value) {
driver.deleteLogFile(image)
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] server is shutting down. Aborting new connection attempts."))
return
}
// ugh, this is verbose -- but necessary
val message = try {
val msg = handshaker.readMessage(buffer, offset, length)
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (msg !is HandshakeMessage) {
throw ServerHandshakeException("[$logInfo] Connection not allowed! unrecognized message: $msg")
} else if (logger.isTraceEnabled) {
logger.trace("[$logInfo] (${msg.connectKey}) received HS: $msg")
}
msg
} catch (e: Exception) {
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error de-serializing handshake message!!", e))
null
}
if (message == null) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
return
}
// NOTE: This MUST to happen in separates thread so that we can take as long as we need when creating publications and handshaking,
// because under load -- this will REGULARLY timeout! Under no circumstance can this happen in the main processing thread!!
server.eventDispatch.HANDSHAKE.launch {
// HandshakeMessage.HELLO
// HandshakeMessage.DONE
val messageState = message.state
val connectKey = message.connectKey
if (messageState == HandshakeMessage.HELLO) {
// we create a NEW publication for the handshake, which connects directly to the client handshake subscription
// we explicitly have the publisher "connect to itself", because we are using MDC to work around NAT.
// It will "auto-connect" to the correct client port (negotiated by the MDC client subscription negotiating on the
// control port of the server)
val publicationUri = uriHandshake(CommonContext.UDP_MEDIA, isReliable)
.controlEndpoint(ipInfo.getAeronPubAddress(isRemoteIpv4) + ":" + mdcPortPub)
// this will always connect to the CLIENT handshake subscription!
val publication = try {
driver.addPublication(publicationUri, message.streamId, logInfo, false)
} catch (e: Exception) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Cannot create publication back to $clientAddressString", e))
return@launch
}
try {
// we actually have to wait for it to connect before we continue.
//
driver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause ->
ServerTimedoutException("$logInfo publication cannot connect with client in ${Sys.getTimePrettyFull(handshakeTimeoutNs)}", cause)
}
} catch (e: Exception) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Cannot create publication back to $clientAddressString", e))
return@launch
}
try {
val success = handshake.processUdpHandshakeMessageServer(
server = server,
handshaker = handshaker,
handshakePublication = publication,
publicKey = message.publicKey!!,
clientAddress = clientAddress,
clientAddressString = clientAddressString,
portPub = message.port,
portSub = serverPortSub,
mdcPortPub = mdcPortPub,
isReliable = isReliable,
message = message,
logInfo = logInfo,
logger = logger
)
if (success) {
publications[connectKey] = publication
} else {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
try {
// we might not be able to close this connection.
driver.close(publication, logInfo)
}
catch (e: Exception) {
server.listenerManager.notifyError(e)
}
}
} catch (e: Exception) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
try {
// we might not be able to close this connection.
driver.close(publication, logInfo)
}
catch (e: Exception) {
driver.close(publication, logInfo)
}
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error processing IPC handshake", e))
}
} else {
// HandshakeMessage.DONE
val publication = publications.remove(connectKey)
if (publication == null) {
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] No publication back to $clientAddressString"))
return@launch
}
try {
handshake.validateMessageTypeAndDoPending(
server = server,
handshaker = handshaker,
handshakePublication = publication,
message = message,
logInfo = logInfo,
logger = logger
)
} catch (e: Exception) {
server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error processing IPC handshake", e))
}
try {
// we might not be able to close this connection.
driver.close(publication, logInfo)
}
catch (e: Exception) {
server.listenerManager.notifyError(e)
}
// we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors
// and connections occur too quickly (within the cleanup/linger period), we can run out of memory!
driver.deleteLogFile(image)
}
}
}
fun close() {
publications.forEach { (connectKey, publication) ->
AeronDriver.sessionIdAllocator.free(publication.sessionId())
try {
// we might not be able to close this connection.
driver.close(publication, "Server Handshake ($connectKey)")
}
catch (e: Exception) {
server.listenerManager.notifyError(e)
}
}
publications.clear()
}
}
fun <CONNECTION : Connection> ipc(server: Server<CONNECTION>, handshake: ServerHandshake<CONNECTION>): AeronPoller {
val logger = server.logger
val connectionFunc = server.connectionFunc
val config = server.config as ServerConfiguration
val poller = if (config.enableIpc) {
val driver = ServerIpcDriver(
streamId = config.ipcId,
sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID
val poller = try {
val driver = ServerHandshakeDriver.build(
aeronDriver = server.aeronDriver,
isIpc = true,
port = 0,
ipInfo = server.ipInfo,
streamIdSub = config.ipcId,
sessionIdSub = AeronDriver.RESERVED_SESSION_ID_INVALID,
logInfo = "HANDSHAKE-IPC"
)
driver.build(aeronDriver, logger)
val subscription = driver.subscription
object : AeronPoller {
val handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
ipcProcessing(logger, server, aeronDriver, header, buffer, offset, length, handshake, connectionFunc)
}
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be:
// - long running
// - re-entrant with the client
val subscription = driver.subscription
val delegate = IpcProc(logger, server, server.aeronDriver, handshake)
val handler = FragmentAssembler(delegate)
override fun poll(): Int {
return subscription.poll(handler, 1)
}
override fun close() {
subscription.close()
delegate.close()
handler.clear()
try {
driver.unsafeClose()
}
catch (ignored: Exception) {
// we are already shutting down, ignore
}
logger.info("Closed IPC poller")
}
override val info = driver.info
override val info = "IPC ${driver.info}"
}
} else {
} catch (e: Exception) {
server.listenerManager.notifyError(ServerException("Unable to create IPC listener.", e))
disabled("IPC Disabled")
}
logger.info { poller.info }
return poller
}
fun <CONNECTION : Connection> ip4(
aeronDriver: AeronDriver, config: ServerConfiguration, server: Server<CONNECTION>, handshake: ServerHandshake<CONNECTION>
): AeronPoller
{
fun <CONNECTION : Connection> ip4(server: Server<CONNECTION>, handshake: ServerHandshake<CONNECTION>): AeronPoller {
val logger = server.logger
val connectionFunc = server.connectionFunc
val config = server.config
val isReliable = config.isReliable
val poller = if (server.canUseIPv4) {
val driver = ServerUdpDriver(
listenAddress = server.listenIPv4Address!!,
port = config.port,
streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID,
sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID,
connectionTimeoutSec = config.connectionCloseTimeoutInSeconds,
isReliable = isReliable
val poller = try {
val driver = ServerHandshakeDriver.build(
aeronDriver = server.aeronDriver,
isIpc = false,
ipInfo = server.ipInfo,
port = server.port1,
streamIdSub = config.udpId,
sessionIdSub = 9,
logInfo = "HANDSHAKE-IPv4"
)
driver.build(aeronDriver, logger)
val subscription = driver.subscription
object : AeronPoller {
/**
* Note:
* Reassembly has been shown to be minimal impact to latency. But not totally negligible. If the lowest latency is
* desired, then limiting message sizes to MTU size is a good practice.
*
* There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB.
* Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery
* properties from failure and streams with mechanical sympathy.
*/
val handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
ipProcessing(logger, server, isReliable, aeronDriver, false, header, buffer, offset, length, handshake, connectionFunc)
}
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be:
// - long running
// - re-entrant with the client
val subscription = driver.subscription
val delegate = UdpProc(logger, server, server.aeronDriver, handshake, isReliable)
val handler = FragmentAssembler(delegate)
override fun poll(): Int {
return subscription.poll(handler, 1)
}
override fun close() {
subscription.close()
delegate.close()
handler.clear()
try {
driver.unsafeClose()
}
catch (ignored: Exception) {
// we are already shutting down, ignore
}
logger.info("Closed IPv4 poller")
}
override val info = "IPv4 ${driver.info}"
}
} else {
} catch (e: Exception) {
server.listenerManager.notifyError(ServerException("Unable to create IPv4 listener.", e))
disabled("IPv4 Disabled")
}
logger.info { poller.info }
return poller
}
fun <CONNECTION : Connection> ip6(
aeronDriver: AeronDriver, config: ServerConfiguration, server: Server<CONNECTION>, handshake: ServerHandshake<CONNECTION>
): AeronPoller
{
fun <CONNECTION : Connection> ip6(server: Server<CONNECTION>, handshake: ServerHandshake<CONNECTION>): AeronPoller {
val logger = server.logger
val connectionFunc = server.connectionFunc
val config = server.config
val isReliable = config.isReliable
val poller = if (server.canUseIPv6) {
val driver = ServerUdpDriver(
listenAddress = server.listenIPv6Address!!,
port = config.port,
streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID,
sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID,
connectionTimeoutSec = config.connectionCloseTimeoutInSeconds,
isReliable = isReliable
val poller = try {
val driver = ServerHandshakeDriver.build(
aeronDriver = server.aeronDriver,
isIpc = false,
ipInfo = server.ipInfo,
port = server.port1,
streamIdSub = config.udpId,
sessionIdSub = 0,
logInfo = "HANDSHAKE-IPv6"
)
driver.build(aeronDriver, logger)
val subscription = driver.subscription
object : AeronPoller {
/**
* Note:
* Reassembly has been shown to be minimal impact to latency. But not totally negligible. If the lowest latency is
* desired, then limiting message sizes to MTU size is a good practice.
*
* There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB.
* Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery
* properties from failure and streams with mechanical sympathy.
*/
val handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
ipProcessing(logger, server, isReliable, aeronDriver, false, header, buffer, offset, length, handshake, connectionFunc)
}
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be:
// - long running
// - re-entrant with the client
val subscription = driver.subscription
val delegate = UdpProc(logger, server, server.aeronDriver, handshake, isReliable)
val handler = FragmentAssembler(delegate)
override fun poll(): Int {
return subscription.poll(handler, 1)
}
override fun close() {
subscription.close()
delegate.close()
handler.clear()
try {
driver.unsafeClose()
}
catch (ignored: Exception) {
// we are already shutting down, ignore
}
logger.info("Closed IPv4 poller")
}
override val info = "IPv6 ${driver.info}"
}
} else {
} catch (e: Exception) {
server.listenerManager.notifyError(ServerException("Unable to create IPv6 listener."))
disabled("IPv6 Disabled")
}
logger.info { poller.info }
return poller
}
fun <CONNECTION : Connection> ip6Wildcard(
aeronDriver: AeronDriver, config: ServerConfiguration, server: Server<CONNECTION>, handshake: ServerHandshake<CONNECTION>
): AeronPoller {
fun <CONNECTION : Connection> ip6Wildcard(server: Server<CONNECTION>, handshake: ServerHandshake<CONNECTION>): AeronPoller {
val logger = server.logger
val connectionFunc = server.connectionFunc
val config = server.config
val isReliable = config.isReliable
val driver = ServerUdpDriver(
listenAddress = server.listenIPv6Address!!,
port = config.port,
streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID,
sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID,
connectionTimeoutSec = config.connectionCloseTimeoutInSeconds,
isReliable = isReliable
)
val poller = try {
val driver = ServerHandshakeDriver.build(
aeronDriver = server.aeronDriver,
isIpc = false,
ipInfo = server.ipInfo,
port = server.port1,
streamIdSub = config.udpId,
sessionIdSub = 0,
logInfo = "HANDSHAKE-IPv4+6"
)
driver.build(aeronDriver, logger)
object : AeronPoller {
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
// publication of any state to other threads and not be:
// - long running
// - re-entrant with the client
val subscription = driver.subscription
val subscription = driver.subscription
val delegate = UdpProc(logger, server, server.aeronDriver, handshake, isReliable)
val handler = FragmentAssembler(delegate)
val poller = object : AeronPoller {
/**
* Note:
* Reassembly has been shown to be minimal impact to latency. But not totally negligible. If the lowest latency is
* desired, then limiting message sizes to MTU size is a good practice.
*
* There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB.
* Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery
* properties from failure and streams with mechanical sympathy.
*/
val handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
ipProcessing(logger, server, isReliable, aeronDriver, true, header, buffer, offset, length, handshake, connectionFunc)
override fun poll(): Int {
return subscription.poll(handler, 1)
}
override fun close() {
delegate.close()
handler.clear()
try {
driver.unsafeClose()
}
catch (ignored: Exception) {
// we are already shutting down, ignore
}
logger.info("Closed IPv4+6 poller")
}
override val info = "IPv4+6 ${driver.info}"
}
override fun poll(): Int {
return subscription.poll(handler, 1)
}
override fun close() {
subscription.close()
}
override val info = "IPv4+6 ${driver.info}"
} catch (e: Exception) {
server.listenerManager.notifyError(ServerException("Unable to create IPv4+6 listeners.", e))
disabled("IPv4+6 Disabled")
}
logger.info { poller.info }
return poller
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 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.handshake;

View File

@ -26,7 +26,7 @@ import java.net.InetAddress
*
* Supports both IPv4 and IPv6.
*/
internal class IpSubnetFilterRule : IpFilterRule {
class IpSubnetFilterRule : IpFilterRule {
private val filterRule: IpFilterRule
constructor(ipAddress: String, cidrPrefix: Int) {

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 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.ipFilter;

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 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;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 dorkbox, llc
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,7 +21,6 @@ class Ping {
var packedId = 0
// ping/pong times are the LOWER 8 bytes of a long, which gives us 65 seconds. This is the same as the max value timeout (a short) so this is acceptable
var pingTime = 0L
var pongTime = 0L

Some files were not shown because too many files have changed in this diff Show More