├── .gitignore ├── Aeron.ClientServer.sln ├── LICENSE.txt ├── NuGet.config ├── README.md ├── nuget ├── pack.bat └── push.bat └── src ├── Aeron.ClientServer.Tests ├── Aeron.ClientServer.Tests.csproj ├── ClientServerTests.cs ├── DriverConfigUtil.cs └── RateLimiterTests.cs └── Aeron.ClientServer ├── Aeron.ClientServer.csproj ├── AeronClient.cs ├── AeronHandshakeRequest.cs ├── AeronMessageType.cs ├── AeronReservedValue.cs ├── AeronResultType.cs ├── AeronServer.cs ├── ClientServerConfig.cs ├── Extensions.cs ├── MachineCounters.cs ├── RateLimiter.cs ├── UdpUtils.cs └── Utils.cs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #ignore thumbnails created by windows 3 | Thumbs.db 4 | #Ignore files build by Visual Studio 5 | *.obj 6 | *.pdb 7 | *.user 8 | *.aps 9 | *.pch 10 | *.vspscc 11 | *_i.c 12 | *_p.c 13 | *.ncb 14 | *.suo 15 | *.tlb 16 | *.tlh 17 | *.bak 18 | *.cache 19 | *.ilk 20 | *.log 21 | *.binlog 22 | [Bb]in 23 | [Dd]ebug*/ 24 | *.lib 25 | *.sbr 26 | *.orig 27 | obj/ 28 | [Rr]elease*/ 29 | _ReSharper*/ 30 | [Tt]est[Rr]esult* 31 | output/** 32 | .vs/ 33 | .idea/ 34 | local-* 35 | nCrunchTemp_* 36 | 37 | lib/packages/** 38 | tools/cake/tools/Cake/* 39 | tools/cake/tools/nuget.exe 40 | build/tools/* 41 | 42 | nuget/artifacts 43 | -------------------------------------------------------------------------------- /Aeron.ClientServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aeron.ClientServer", "src\Aeron.ClientServer\Aeron.ClientServer.csproj", "{B6FD9558-5E6C-4091-A52C-315CCF92EFE2}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aeron.ClientServer.Tests", "src\Aeron.ClientServer.Tests\Aeron.ClientServer.Tests.csproj", "{BB3CA50D-9467-49EB-8BD5-A08A21817B50}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6701D877-9076-47B0-A46B-919E6C8F83EF}" 11 | ProjectSection(SolutionItems) = preProject 12 | .gitignore = .gitignore 13 | LICENSE.txt = LICENSE.txt 14 | NuGet.config = NuGet.config 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Debug|x64.ActiveCfg = Debug|Any CPU 34 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Debug|x64.Build.0 = Debug|Any CPU 35 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Debug|x86.ActiveCfg = Debug|Any CPU 36 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Debug|x86.Build.0 = Debug|Any CPU 37 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Release|x64.ActiveCfg = Release|Any CPU 40 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Release|x64.Build.0 = Release|Any CPU 41 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Release|x86.ActiveCfg = Release|Any CPU 42 | {B6FD9558-5E6C-4091-A52C-315CCF92EFE2}.Release|x86.Build.0 = Release|Any CPU 43 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Debug|x64.ActiveCfg = Debug|Any CPU 46 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Debug|x64.Build.0 = Debug|Any CPU 47 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Debug|x86.ActiveCfg = Debug|Any CPU 48 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Debug|x86.Build.0 = Debug|Any CPU 49 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Release|x64.ActiveCfg = Release|Any CPU 52 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Release|x64.Build.0 = Release|Any CPU 53 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Release|x86.ActiveCfg = Release|Any CPU 54 | {BB3CA50D-9467-49EB-8BD5-A08A21817B50}.Release|x86.Build.0 = Release|Any CPU 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Abc Arbitrage Asset Management 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Client/Server implementation using Aeron as a transport 2 | 3 | This is work in progress. The code depends on a PR to Aeron.NET, to where we have moved 4 | native parts from this repository. Until the updated NuGets are published, 5 | they have to be built manually and added as dependency for this project to work. 6 | 7 | -------------------------------------------------------------------------------- /nuget/pack.bat: -------------------------------------------------------------------------------- 1 | del .\artifacts\*.nupkg 2 | 3 | dotnet restore ..\src\Aeron.ClientServer 4 | dotnet pack ..\src\Aeron.ClientServer -c Release -o .\artifacts 5 | 6 | pause -------------------------------------------------------------------------------- /nuget/push.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | dir .\artifacts 4 | 5 | setlocal 6 | :PROMPT 7 | SET /P AREYOUSURE=Push to NuGet.org? (Y/[N])? 8 | IF /I "%AREYOUSURE%" NEQ "Y" GOTO END 9 | 10 | @for %%f in (.\artifacts\*.nupkg) do dotnet nuget push %%f --source https://www.nuget.org/api/v2/package --timeout 10000 11 | 12 | :END 13 | endlocal 14 | 15 | pause -------------------------------------------------------------------------------- /src/Aeron.ClientServer.Tests/Aeron.ClientServer.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | false 6 | true 7 | x64 8 | Abc.Aeron.ClientServer.Tests 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer.Tests/ClientServerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading; 5 | using NUnit.Framework; 6 | 7 | namespace Abc.Aeron.ClientServer.Tests 8 | { 9 | [TestFixture] 10 | public class ClientServerTests 11 | { 12 | [Test, Explicit("manual")] 13 | public void CouldStartClientServer() 14 | { 15 | const bool deleteOnShutdown = true; 16 | var baseDir = @"D:\tmp\aeron"; 17 | var serverDir = Path.Combine(baseDir!, "server-" + Guid.NewGuid().ToString("N")); 18 | var clientDir = Path.Combine(baseDir!, "client-" + Guid.NewGuid().ToString("N")); 19 | 20 | var config = ClientServerConfig.SharedNetworkSleeping(serverDir); 21 | config.DirDeleteOnShutdown = deleteOnShutdown; 22 | 23 | var serverPort = 43210; 24 | var server = new AeronServer(serverPort, config); 25 | 26 | server.MessageReceived += (identity, message) => 27 | { 28 | Console.WriteLine("SERVER: MESSAGE RECEIVED"); 29 | server.Send((int)identity, message); 30 | }; 31 | 32 | server.ClientConnected += l => 33 | { 34 | Console.WriteLine($"SERVER: CLIENT CONNECTED {l}"); 35 | }; 36 | 37 | server.ClientDisconnected += l => 38 | { 39 | Console.WriteLine($"SERVER: CLIENT DISCONNECTED {l}"); 40 | }; 41 | 42 | server.Start(); 43 | 44 | var mre = new ManualResetEventSlim(false); 45 | var connectedMre = new ManualResetEventSlim(false); 46 | 47 | var client = AeronClient.GetClientReference(clientDir, 48 | dir => 49 | { 50 | var config = ClientServerConfig.SharedNetworkSleeping(dir); 51 | config.DirDeleteOnShutdown = deleteOnShutdown; 52 | return config; 53 | }); 54 | client.Start(); 55 | 56 | var timeoutMs = 10000; 57 | byte[] received = null; 58 | var connectionId = client.Connect("127.0.0.1", 59 | serverPort, 60 | () => 61 | { 62 | Console.WriteLine("CLIENT: CONNECTED"); 63 | connectedMre.Set(); 64 | }, 65 | () => 66 | { 67 | Console.WriteLine("CLIENT: DISCONNECTED"); 68 | }, 69 | message => 70 | { 71 | Console.WriteLine($"CLIENT: MESSAGE RECEIVED with length: {message.Length}"); 72 | received = message.ToArray(); 73 | mre.Set(); 74 | }, 75 | timeoutMs); 76 | 77 | connectedMre.Wait(); 78 | var sent = new byte[] { 1, 2, 3, 4, 5 }; 79 | client.Send(connectionId, sent); 80 | 81 | mre.Wait(timeoutMs); 82 | Assert.True(sent.SequenceEqual(received)); 83 | 84 | client.Disconnect(connectionId); 85 | 86 | Thread.Sleep(1000); 87 | 88 | client.Dispose(); 89 | 90 | server.Dispose(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer.Tests/DriverConfigUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Adaptive.Aeron.Driver.Native; 4 | 5 | namespace Abc.Aeron.ClientServer.Tests 6 | { 7 | public static class DriverContextUtil 8 | { 9 | public static AeronDriver.DriverContext CreateDriverCtx(bool isServer = false) 10 | { 11 | var baseDir = AppDomain.CurrentDomain.BaseDirectory; 12 | var dir = Path.Combine(baseDir, "mediadrivers", (isServer ? "server_" : "") + Guid.NewGuid().ToString("N")); 13 | 14 | var ctx = new AeronDriver.DriverContext() 15 | .AeronDirectoryName(dir) 16 | // .DebugTimeoutMs(60 * 1000) 17 | .ThreadingMode(AeronThreadingModeEnum.AeronThreadingModeShared) 18 | .SharedIdleStrategy(DriverIdleStrategy.SLEEPING) 19 | .TermBufferLength(128 * 1024) 20 | .DirDeleteOnStart(true) 21 | .DirDeleteOnShutdown(true) 22 | .PrintConfigurationOnStart(true) 23 | .LoggerInfo(s => Console.WriteLine($"INFO: {s}")) 24 | .LoggerWarning(s => Console.WriteLine($"WARN: {s}")) 25 | .LoggerError(s => Console.Error.WriteLine($"ERROR: {s}")); 26 | 27 | return ctx; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer.Tests/RateLimiterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using NUnit.Framework; 8 | 9 | namespace Abc.Aeron.ClientServer.Tests 10 | { 11 | [TestFixture] 12 | [SuppressMessage("ReSharper", "HeapView.ClosureAllocation")] 13 | public class RateLimiterTests 14 | { 15 | private volatile bool _isRunning; 16 | private static readonly Stopwatch _sw = new Stopwatch(); 17 | private static long _counter; 18 | private const int _iterations = 10_000_000; 19 | 20 | [Test, Explicit("manual long running")] 21 | public async Task RateLimiterNeitherExceedsLimitNorDrifts() 22 | { 23 | (long newHead, long newTicks)[] data = new (long, long)[_iterations]; 24 | 25 | _isRunning = true; 26 | var rateLimiter = new RateLimiter(); 27 | var writers = Math.Max(3, Environment.ProcessorCount / 2); 28 | Console.WriteLine($"Using {writers} number of writers"); 29 | var tasks = new Task[writers]; 30 | 31 | for (int i = 0; i < writers; i++) 32 | { 33 | var ii = i; 34 | tasks[i] = Task.Factory.StartNew(() => 35 | { 36 | var c = 0; 37 | 38 | while (_isRunning) 39 | { 40 | var bufferSize = 97 * (1 + ii); 41 | rateLimiter.ApplyRateLimit(bufferSize, out var newHead, out var newTicks); 42 | var idx = Interlocked.Increment(ref _counter); 43 | if(idx >= _iterations) 44 | return; 45 | 46 | data[idx] = (newHead, newTicks); 47 | c++; 48 | } 49 | }, TaskCreationOptions.LongRunning); 50 | } 51 | 52 | await Task.WhenAll(tasks); 53 | 54 | data = data.OrderBy(x => x.Item2).ToArray(); 55 | 56 | double previousTicks = data[0].newTicks; 57 | double previousWritten = data[0].newHead; 58 | double previousMaxBw = 0; 59 | 60 | 61 | for (int i = 1; i < _iterations; i++) 62 | { 63 | var currentTicks = data[i].newTicks; 64 | var currentWritten = data[i].newHead; 65 | 66 | if (currentWritten - previousWritten >= RateLimiter.CHUNK_SIZE) 67 | { 68 | var bw = Math.Round((8 * (currentWritten - previousWritten) / ((currentTicks - previousTicks) / Stopwatch.Frequency)) / (1024 * 1024), 3); 69 | 70 | if (bw > previousMaxBw) 71 | { 72 | Console.WriteLine($"New Max BW: {bw:N2} over {currentWritten - previousWritten:N0} bytes"); 73 | previousMaxBw = bw; 74 | } 75 | 76 | if (bw > 1.5 * rateLimiter.BwLimitBytes * 8 / (1024 * 1024)) 77 | { 78 | Console.WriteLine($"High BW: {bw:N2}"); 79 | } 80 | 81 | if (i % 1000 == 0) 82 | { 83 | Console.WriteLine($"BW: {bw:N2}, Max BW: {previousMaxBw:N2}"); 84 | } 85 | 86 | previousTicks = currentTicks; 87 | previousWritten = currentWritten; 88 | 89 | } 90 | } 91 | 92 | _isRunning = false; 93 | 94 | Console.WriteLine("Finished..."); 95 | } 96 | 97 | private static void NOP(double durationSeconds) 98 | { 99 | _sw.Restart(); 100 | if (Math.Abs(durationSeconds) < 0.000_000_01) 101 | { 102 | return; 103 | } 104 | 105 | var durationTicks = Math.Round(durationSeconds * Stopwatch.Frequency); 106 | 107 | while (_sw.ElapsedTicks < durationTicks) 108 | { 109 | Thread.SpinWait(1); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/Aeron.ClientServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Client/Server implementation using Aeron as a transport. 4 | netstandard2.0 5 | enable 6 | 8 7 | true 8 | x64 9 | $(NoWarn),1591 10 | Abc.Aeron.ClientServer 11 | 0.3.0 12 | Aeron Client Server 13 | https://github.com/Abc-Arbitrage/Aeron.ClientServer 14 | https://github.com/Abc-Arbitrage/Aeron.ClientServer.git 15 | Abc.Aeron.ClientServer 16 | Victor Baybekov, Lucas Trzesniewski 17 | Abc Arbitrage Asset Management 18 | Abc Arbitrage Asset Management 2021 19 | MIT 20 | embedded 21 | true 22 | true 23 | Abc.Aeron.ClientServer 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/AeronClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Net.Sockets; 8 | using System.Runtime.CompilerServices; 9 | using System.Runtime.InteropServices; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Adaptive.Aeron; 13 | using Adaptive.Aeron.Driver.Native; 14 | using Adaptive.Aeron.Exceptions; 15 | using Adaptive.Aeron.LogBuffer; 16 | using Adaptive.Agrona; 17 | using Adaptive.Agrona.Concurrent; 18 | using log4net; 19 | using ProtoBuf; 20 | 21 | namespace Abc.Aeron.ClientServer 22 | { 23 | public delegate void AeronClientMessageReceivedHandler(ReadOnlySpan message); 24 | 25 | public class AeronClient : IDisposable 26 | { 27 | private static readonly ILog _log = LogManager.GetLogger(typeof(AeronClient)); 28 | private static readonly ILog _driverLog = LogManager.GetLogger(typeof(AeronDriver)); 29 | 30 | private static readonly object _clientsLock = new object(); 31 | 32 | private static readonly Dictionary _clients = 33 | new Dictionary(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) 34 | ? StringComparer.Ordinal 35 | : StringComparer.OrdinalIgnoreCase); 36 | 37 | private int _clientPort; 38 | private const int _frameCountLimit = 16384; 39 | private int _roundRobinIndex; 40 | private int _referenceCount; 41 | 42 | private volatile bool _isRunning; 43 | private bool _isTerminatedUnexpectedly; 44 | 45 | private readonly object _connectionsLock = new object(); 46 | private AeronClientSession?[] _clientSessions; 47 | 48 | private readonly ClientServerConfig _config; 49 | private readonly AeronDriver _driver; 50 | private readonly Adaptive.Aeron.Aeron _client; 51 | 52 | private Thread? _receiveThread; 53 | 54 | public event Action? TerminatedUnexpectedly; 55 | 56 | private AeronClient(ClientServerConfig config) 57 | { 58 | _config = config; 59 | AeronDriver.DriverContext driverContext = config.ToDriverContext(); 60 | Adaptive.Aeron.Aeron.Context clientContext = config.ToClientContext(); 61 | 62 | driverContext 63 | .LoggerInfo(_driverLog.Info) 64 | .LoggerWarning(_driverLog.Warn) 65 | .LoggerWarning(_driverLog.Error); 66 | 67 | _driver = AeronDriver.Start(driverContext); 68 | 69 | clientContext 70 | .ErrorHandler(OnError) 71 | .AvailableImageHandler(ConnectionOnImageAvailable) 72 | .UnavailableImageHandler(ConnectionOnImageUnavailable); 73 | 74 | _client = Adaptive.Aeron.Aeron.Connect(clientContext); 75 | 76 | const int sessionsLen = 77 | #if DEBUG 78 | 1; 79 | #else 80 | 64; 81 | #endif 82 | 83 | _clientSessions = new AeronClientSession[sessionsLen]; 84 | } 85 | 86 | private void OnError(Exception exception) 87 | { 88 | _driverLog.Error("Aeron connection error", exception); 89 | 90 | if (_client.IsClosed) 91 | return; 92 | 93 | switch (exception) 94 | { 95 | case AeronException _: 96 | case AgentTerminationException _: 97 | _log.Error("Unrecoverable Media Driver error"); 98 | ConnectionOnTerminatedUnexpectedly(); 99 | break; 100 | } 101 | } 102 | 103 | public int Connect(string serverHost, 104 | int serverPort, 105 | Action onConnectedDelegate, 106 | Action onDisconnectedDelegate, 107 | AeronClientMessageReceivedHandler onMessageReceived, 108 | int connectionResponseTimeoutMs) 109 | { 110 | if (serverHost == null) 111 | throw new ArgumentNullException(nameof(serverHost)); 112 | 113 | if (_isTerminatedUnexpectedly) 114 | return -1; 115 | 116 | string serverHostIp; 117 | if (serverHost.Equals("localhost", StringComparison.OrdinalIgnoreCase) || serverHost.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase)) 118 | { 119 | // TODO IPC channel for local communication 120 | serverHostIp = "127.0.0.1"; 121 | } 122 | else 123 | { 124 | var ipv4 = 125 | Dns.GetHostEntry(serverHost).AddressList 126 | .FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork) 127 | ?? throw new ArgumentException($"Cannot resolve serverHost ip for {serverHost}"); 128 | serverHostIp = ipv4.ToString(); 129 | } 130 | 131 | var serverChannel = Utils.RemoteChannel(serverHostIp, serverPort); 132 | var publication = _client.AddExclusivePublication(serverChannel, AeronServer.ServerStreamId); 133 | 134 | var localIp = UdpUtils.GetLocalIPAddress(serverHostIp, serverPort); 135 | var streamId = MachineCounters.Instance.GetNewClientStreamId(); 136 | GetChannelAndSubscription(localIp, streamId, out var clientChannel, out var subscription); 137 | 138 | var session = new AeronClientSession(this, 139 | serverChannel, 140 | publication, 141 | subscription, 142 | onConnectedDelegate, 143 | onDisconnectedDelegate, 144 | onMessageReceived); 145 | var connectionId = AddSession(session); 146 | 147 | _log.Info($"Connecting: {session}"); 148 | 149 | var handshakeRequest = new AeronHandshakeRequest 150 | { 151 | Channel = clientChannel, 152 | StreamId = streamId 153 | }; 154 | 155 | if (!session.SendHandshake(handshakeRequest, connectionResponseTimeoutMs)) 156 | { 157 | RemoveSession(session); 158 | session.Dispose(); 159 | return -1; 160 | } 161 | 162 | return connectionId; 163 | } 164 | 165 | private void GetChannelAndSubscription(string localIp, 166 | int streamId, 167 | out string clientChannel, 168 | out Subscription subscription) 169 | { 170 | // Finding port for the first time is slow and in tests with 171 | // parallel connections many clients have different ports. 172 | // Simple lock is needed. 173 | lock (_connectionsLock) 174 | { 175 | while (true) 176 | { 177 | if (_clientPort == 0) 178 | { 179 | var clientPort = 0; 180 | while (true) 181 | { 182 | try 183 | { 184 | clientPort = UdpUtils.GetRandomUnusedPort(); 185 | clientChannel = Utils.RemoteChannel(localIp, clientPort); 186 | subscription = _client.AddSubscription(clientChannel, streamId); 187 | break; 188 | } 189 | catch (RegistrationException ex) 190 | { 191 | // if port was already reused by someone 192 | _log.Warn($"Could not subscribe on new port {clientPort}", ex); 193 | } 194 | } 195 | 196 | _clientPort = clientPort; 197 | return; 198 | } 199 | 200 | try 201 | { 202 | clientChannel = Utils.RemoteChannel(localIp, _clientPort); 203 | subscription = _client.AddSubscription(clientChannel, streamId); 204 | return; 205 | } 206 | catch (RegistrationException ex) 207 | { 208 | _log.Warn($"Could not subscribe on existing port {_clientPort}", ex); 209 | _clientPort = 0; 210 | } 211 | } 212 | } 213 | } 214 | 215 | private int AddSession(AeronClientSession session) 216 | { 217 | if (session == null) 218 | throw new ArgumentNullException(nameof(session)); 219 | 220 | var connectionId = 0; 221 | 222 | lock (_connectionsLock) 223 | { 224 | for (var i = 0; i < _clientSessions.Length; i++) 225 | { 226 | if (_clientSessions[i] == null) 227 | { 228 | connectionId = ConnectionIndexToId(i); 229 | _clientSessions[i] = session; 230 | break; 231 | } 232 | } 233 | 234 | if (connectionId == 0) 235 | { 236 | var newSessions = new AeronClientSession[_clientSessions.Length * 2]; 237 | _clientSessions.CopyTo(newSessions, 0); 238 | connectionId = ConnectionIndexToId(_clientSessions.Length); 239 | newSessions[_clientSessions.Length] = session; 240 | _clientSessions = newSessions; 241 | } 242 | } 243 | 244 | return connectionId; 245 | } 246 | 247 | private AeronClientSession? RemoveSession(int connectionId) 248 | { 249 | lock (_connectionsLock) 250 | { 251 | var sessionIndex = ConnectionIdToIndex(connectionId); 252 | if ((uint)sessionIndex >= _clientSessions.Length) 253 | throw new IndexOutOfRangeException(nameof(connectionId)); 254 | 255 | var session = _clientSessions[sessionIndex]; 256 | _clientSessions[sessionIndex] = null; 257 | return session; 258 | } 259 | } 260 | 261 | private void RemoveSession(AeronClientSession session) 262 | { 263 | lock (_connectionsLock) 264 | { 265 | for (var i = 0; i < _clientSessions.Length; i++) 266 | { 267 | if (_clientSessions[i] == session) 268 | { 269 | _clientSessions[i] = null; 270 | break; 271 | } 272 | } 273 | } 274 | } 275 | 276 | public void Disconnect(int connectionId) 277 | => Disconnect(connectionId, true); 278 | 279 | private void Disconnect(int connectionId, bool sendNotification) 280 | { 281 | if (connectionId == 0) 282 | return; 283 | 284 | var session = RemoveSession(connectionId); 285 | if (session == null) 286 | return; 287 | 288 | _log.Info($"Disconnecting: {session}"); 289 | 290 | if (sendNotification) 291 | { 292 | Debug.Assert(_receiveThread != Thread.CurrentThread, 293 | "Notification is only sent from FeedClient.Stop method"); 294 | // Notify server that we are disconnecting. There should be no ack from server. 295 | session.SendDisconnectNotification(); 296 | } 297 | 298 | session.Dispose(); 299 | } 300 | 301 | private void SessionDisconnected(AeronClientSession session) 302 | { 303 | RemoveSession(session); 304 | session.OnDisconnected.Invoke(); 305 | session.Dispose(); 306 | } 307 | 308 | private void ConnectionOnImageAvailable(Image image) 309 | { 310 | var subscription = image.Subscription; 311 | if (subscription == null) 312 | return; 313 | 314 | if (_log.IsDebugEnabled) 315 | _log.Debug( 316 | $"Available image on {subscription.Channel} streamId={subscription.StreamId} sessionId={image.SessionId} from {image.SourceIdentity}"); 317 | 318 | lock (_connectionsLock) 319 | { 320 | foreach (var session in _clientSessions) 321 | { 322 | if (session?.Subscription == subscription) 323 | { 324 | session.SetSubscriptionImageAvailable(); 325 | break; 326 | } 327 | } 328 | } 329 | } 330 | 331 | private void ConnectionOnImageUnavailable(Image image) 332 | { 333 | var subscription = image.Subscription; 334 | if (subscription == null) 335 | return; 336 | 337 | lock (_connectionsLock) 338 | { 339 | for (var i = 0; i < _clientSessions.Length; i++) 340 | { 341 | var session = _clientSessions[i]; 342 | 343 | if (session?.Subscription == subscription) 344 | { 345 | // Do not send a disconnect notification, server is unavailable 346 | _clientSessions[i] = null; 347 | session.OnDisconnected.Invoke(); 348 | session.Dispose(); 349 | break; 350 | } 351 | } 352 | } 353 | 354 | if (_log.IsDebugEnabled) 355 | _log.Debug( 356 | $"Unavailable image on {subscription.Channel} streamId={subscription.StreamId} sessionId={image.SessionId} from {image.SourceIdentity}"); 357 | } 358 | 359 | public void Start() 360 | { 361 | if (_isRunning) 362 | return; 363 | 364 | _isRunning = true; 365 | _receiveThread = new Thread(PollThread) 366 | { 367 | IsBackground = true, 368 | Name = "AeronClient Poll Thread" 369 | }; 370 | 371 | _receiveThread.Start(); 372 | } 373 | 374 | private void PollThread() 375 | { 376 | _log.Info($"AeronClient receive thread started with thread id: {Thread.CurrentThread.ManagedThreadId}"); 377 | 378 | try 379 | { 380 | var idleStrategy = _config.ClientIdleStrategy.GetClientIdleStrategy(); 381 | 382 | while (_isRunning) 383 | { 384 | var count = Poll(); 385 | idleStrategy.Idle(count); 386 | } 387 | } 388 | catch (Exception ex) 389 | { 390 | _log.Error("Unhandled exception in receive thread", ex); 391 | throw; 392 | } 393 | } 394 | 395 | private int Poll() 396 | { 397 | // ReSharper disable once InconsistentlySynchronizedField : atomic reference swap on growth 398 | var clientSessions = _clientSessions; 399 | 400 | var length = clientSessions.Length; 401 | var count = 0; 402 | var startIndex = _roundRobinIndex; 403 | int index; 404 | if (startIndex >= length) 405 | _roundRobinIndex = startIndex = 0; 406 | 407 | var lastPolledClient = startIndex; 408 | 409 | for (index = startIndex; index < length && count < _frameCountLimit; ++index) 410 | { 411 | var session = clientSessions[index]; 412 | lastPolledClient = index; 413 | 414 | if (session is null) 415 | continue; 416 | 417 | count += session.Poll(_frameCountLimit - count); 418 | } 419 | 420 | for (index = 0; index < startIndex && count < _frameCountLimit; ++index) 421 | { 422 | var session = clientSessions[index]; 423 | lastPolledClient = index; 424 | 425 | if (session is null) 426 | continue; 427 | 428 | count += session.Poll(_frameCountLimit - count); 429 | } 430 | 431 | _roundRobinIndex = lastPolledClient + 1; 432 | 433 | return count; 434 | } 435 | 436 | public void Send(int connectionId, ReadOnlySpan message) 437 | { 438 | var session = GetSession(connectionId); 439 | session?.Send(message); 440 | } 441 | 442 | private AeronClientSession? GetSession(int connectionId) 443 | { 444 | if (connectionId <= 0) 445 | return null; 446 | 447 | // ReSharper disable once InconsistentlySynchronizedField : atomic reference swap on growth 448 | var clientSessions = _clientSessions; 449 | 450 | var connectionIndex = ConnectionIdToIndex(connectionId); 451 | if ((uint)connectionIndex >= clientSessions.Length) 452 | return null; 453 | 454 | return clientSessions[connectionIndex]; 455 | } 456 | 457 | private void ConnectionOnTerminatedUnexpectedly() 458 | { 459 | _isTerminatedUnexpectedly = true; 460 | TerminatedUnexpectedly?.Invoke(); 461 | Dispose(); 462 | } 463 | 464 | public void DisposeReferenceCounted() 465 | { 466 | lock (_clientsLock) 467 | { 468 | if (--_referenceCount > 0) 469 | return; 470 | 471 | Dispose(); 472 | } 473 | } 474 | 475 | public void Dispose() 476 | { 477 | lock (_clientsLock) 478 | { 479 | if (!_isRunning) 480 | return; 481 | 482 | _isRunning = false; 483 | 484 | if (_receiveThread != null) 485 | { 486 | if (_receiveThread.Join(1000)) 487 | _log.Info("Exited receive thread"); 488 | else 489 | _log.Warn("Timed out when joining receive thread"); 490 | } 491 | 492 | try 493 | { 494 | lock (_connectionsLock) 495 | { 496 | for (var i = 0; i < _clientSessions.Length; i++) 497 | { 498 | if (_clientSessions[i] == null) 499 | continue; 500 | 501 | _clientSessions[i]?.Dispose(); 502 | _clientSessions[i] = null; 503 | } 504 | } 505 | 506 | try 507 | { 508 | _client.Dispose(); 509 | _driver.Dispose(); // last 510 | } 511 | catch(AeronDriver.MediaDriverException) 512 | { 513 | try 514 | { 515 | if (_config.DirDeleteOnShutdown && Directory.Exists(_config.Dir)) 516 | { 517 | Thread.Sleep(100); 518 | Directory.Delete(_config.Dir, true); 519 | } 520 | } 521 | catch (Exception ex) 522 | { 523 | Console.WriteLine($"Cannot delete client dir [{_config.Dir}] on shutdown:\n{ex}"); 524 | _log.Warn($"Cannot delete client dir [{_config.Dir}] on shutdown:\n{ex}"); 525 | } 526 | } 527 | } 528 | finally 529 | { 530 | _clients.Remove(_config.Dir); 531 | } 532 | } 533 | } 534 | 535 | public static AeronClient GetClientReference(string path, Func? mdConfigFactory) 536 | { 537 | path = Path.GetFullPath(path); 538 | 539 | lock (_clientsLock) 540 | { 541 | if (!_clients.TryGetValue(path, out var client)) 542 | { 543 | client = new AeronClient(mdConfigFactory?.Invoke(path) ?? new ClientServerConfig(path)); 544 | _clients.Add(path, client); 545 | } 546 | 547 | client._referenceCount++; 548 | return client; 549 | } 550 | } 551 | 552 | internal void DisposeDriver() => _driver.Dispose(); 553 | 554 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 555 | private static int ConnectionIndexToId(int connectionIndex) 556 | => connectionIndex + 1; 557 | 558 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 559 | private static int ConnectionIdToIndex(int connectionId) 560 | => connectionId - 1; 561 | 562 | private class AeronClientSession : IDisposable 563 | { 564 | private readonly AeronClient _client; 565 | private readonly string _serverChannel; 566 | private readonly Publication _publication; 567 | private readonly UnsafeBuffer _buffer; 568 | public readonly Subscription Subscription; 569 | public readonly Action OnDisconnected; 570 | private readonly AeronClientMessageReceivedHandler _onMessageReceived; 571 | 572 | private readonly Action _onConnected; 573 | 574 | private bool _isConnected; 575 | private volatile bool _isSubscriptionImageAvailable; 576 | private bool _isDisposed; 577 | private readonly FragmentAssembler _fragmentAssembler; 578 | 579 | private int _serverAssignedSessionId; 580 | private ReservedValueSupplier? _dataReservedValueSupplier; 581 | 582 | public AeronClientSession(AeronClient client, 583 | string serverChannel, 584 | Publication publication, 585 | Subscription subscription, 586 | Action onConnected, 587 | Action onDisconnected, 588 | AeronClientMessageReceivedHandler onMessageReceived) 589 | { 590 | _client = client; 591 | _serverChannel = serverChannel; 592 | _publication = publication; 593 | _buffer = new UnsafeBuffer(); 594 | Subscription = subscription; 595 | OnDisconnected = onDisconnected; 596 | _onMessageReceived = onMessageReceived; 597 | _onConnected = onConnected; 598 | _isConnected = false; 599 | _fragmentAssembler = new FragmentAssembler(HandlerHelper.ToFragmentHandler(SubscriptionHandler)); 600 | } 601 | 602 | private bool IsConnected => _isConnected && _isSubscriptionImageAvailable && !_isDisposed; 603 | 604 | public void Dispose() 605 | { 606 | _isSubscriptionImageAvailable = false; 607 | 608 | if (_isDisposed) 609 | return; 610 | 611 | _isDisposed = true; 612 | 613 | Task.Factory.StartNew( 614 | async x => 615 | { 616 | // need to sleep, otherwise fast restart of a client connection causes segfault (e.g. should_allow_to_stop_and_start_client test) 617 | await Task.Delay(1000); 618 | 619 | var session = (AeronClientSession)x!; 620 | 621 | session._publication.Dispose(); 622 | session.Subscription.Dispose(); 623 | session._buffer.Dispose(); 624 | }, 625 | this 626 | ); 627 | } 628 | 629 | public void SetSubscriptionImageAvailable() 630 | { 631 | _isSubscriptionImageAvailable = true; 632 | } 633 | 634 | private void SetConnected(int serverAssignedSessionId) 635 | { 636 | _serverAssignedSessionId = serverAssignedSessionId; 637 | 638 | var dataReservedValue = (long)new AeronReservedValue(Utils.CurrentProtocolVersion, 639 | AeronMessageType.Data, 640 | serverAssignedSessionId); 641 | _dataReservedValueSupplier = (buffer, offset, length) => dataReservedValue; 642 | 643 | _isConnected = true; 644 | _onConnected.Invoke(); 645 | } 646 | 647 | public unsafe void Send(ReadOnlySpan message) 648 | { 649 | if (!IsConnected) 650 | throw new InvalidOperationException("Trying to send when not connected"); 651 | 652 | fixed (byte* ptr = message) 653 | { 654 | _buffer.Wrap(ptr, message.Length); 655 | 656 | var spinWait = new SpinWait(); 657 | 658 | while (true) 659 | { 660 | var errorCode = _publication.Offer(_buffer, 0, message.Length, _dataReservedValueSupplier); 661 | 662 | if (errorCode >= 0) 663 | break; 664 | 665 | var result = Utils.InterpretPublicationOfferResult(errorCode); 666 | 667 | if (result == AeronResultType.Success) 668 | break; 669 | 670 | if (result == AeronResultType.ShouldRetry) 671 | { 672 | spinWait.SpinOnce(); 673 | continue; 674 | } 675 | 676 | _client.SessionDisconnected(this); 677 | return; 678 | } 679 | 680 | _buffer.Release(); 681 | } 682 | } 683 | 684 | public bool SendHandshake(AeronHandshakeRequest handshakeRequest, int connectionResponseTimeoutMs) 685 | { 686 | using var handshakeRequestStream = new MemoryStream(); 687 | Serializer.SerializeWithLengthPrefix(handshakeRequestStream, handshakeRequest, PrefixStyle.Base128); 688 | _buffer.Wrap(handshakeRequestStream.GetBuffer(), 0, (int)handshakeRequestStream.Length); 689 | 690 | var spinWait = new SpinWait(); 691 | var stopwatch = Stopwatch.StartNew(); 692 | 693 | while (true) 694 | { 695 | var errorCode = _publication.Offer(_buffer, 696 | 0, 697 | (int)handshakeRequestStream.Length, 698 | (buffer, offset, length) => (long)new AeronReservedValue(Utils.CurrentProtocolVersion, 699 | AeronMessageType.Connected, 700 | 0)); 701 | 702 | if (errorCode == Publication.NOT_CONNECTED) 703 | { 704 | // This will happen as we just created the publication - we need to wait for Aeron to do its stuff 705 | 706 | if (stopwatch.ElapsedMilliseconds > connectionResponseTimeoutMs) 707 | { 708 | _log.Error($"Failed to send handshake (not connected): {this}"); 709 | return false; 710 | } 711 | 712 | spinWait.SpinOnce(); 713 | continue; 714 | } 715 | 716 | var result = Utils.InterpretPublicationOfferResult(errorCode); 717 | 718 | if (result == AeronResultType.Success) 719 | break; 720 | 721 | if (result == AeronResultType.ShouldRetry) 722 | { 723 | spinWait.SpinOnce(); 724 | continue; 725 | } 726 | 727 | _log.Error($"Failed to send handshake: {this}"); 728 | return false; 729 | } 730 | 731 | _buffer.Release(); 732 | return true; 733 | } 734 | 735 | public void SendDisconnectNotification() 736 | { 737 | while (true) 738 | { 739 | var errorCode = _publication.Offer(_buffer, 740 | 0, 741 | 0, 742 | (buffer, offset, length) => (long)new AeronReservedValue(Utils.CurrentProtocolVersion, 743 | AeronMessageType.Disconnected, 744 | _serverAssignedSessionId)); 745 | var result = Utils.InterpretPublicationOfferResult(errorCode); 746 | 747 | if (result == AeronResultType.ShouldRetry) 748 | { 749 | Thread.SpinWait(1); 750 | continue; 751 | } 752 | 753 | break; 754 | } 755 | } 756 | 757 | public int Poll(int maxCount) 758 | { 759 | return _isSubscriptionImageAvailable ? Subscription.Poll(_fragmentAssembler, maxCount) : 0; 760 | } 761 | 762 | private unsafe void SubscriptionHandler(IDirectBuffer buffer, int offset, int length, Header header) 763 | { 764 | var reservedValue = (AeronReservedValue)header.ReservedValue; 765 | 766 | if (reservedValue.ProtocolVersion != Utils.CurrentProtocolVersion) 767 | { 768 | _log.Error( 769 | $"Received message with unsupported protocol version: {reservedValue.ProtocolVersion} from {this}, ignoring"); 770 | return; 771 | } 772 | 773 | switch (reservedValue.MessageType) 774 | { 775 | case AeronMessageType.Data: 776 | _onMessageReceived(new ReadOnlySpan((byte*)buffer.BufferPointer + offset, length)); 777 | break; 778 | 779 | case AeronMessageType.Connected: 780 | // set server-side session for subsequent use in Send 781 | SetConnected(reservedValue.SessionId); 782 | break; 783 | 784 | case AeronMessageType.Disconnected: 785 | // graceful disconnect notification from the server 786 | _client.SessionDisconnected(this); 787 | break; 788 | } 789 | 790 | // ignore other reserved values 791 | } 792 | 793 | public override string ToString() => 794 | $"{_serverChannel}, Publication: {_publication.SessionId}, Subscription StreamId: {Subscription.StreamId}"; 795 | } 796 | } 797 | } 798 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/AeronHandshakeRequest.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf; 2 | 3 | namespace Abc.Aeron.ClientServer 4 | { 5 | [ProtoContract] 6 | internal class AeronHandshakeRequest 7 | { 8 | [ProtoMember(1, IsRequired = false)] 9 | public string Channel { get; set; } = default!; 10 | 11 | [ProtoMember(2, IsRequired = false)] 12 | public int StreamId { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/AeronMessageType.cs: -------------------------------------------------------------------------------- 1 | namespace Abc.Aeron.ClientServer 2 | { 3 | /// 4 | /// Reserved field tag 5 | /// 6 | internal enum AeronMessageType : byte 7 | { 8 | Data = 0, 9 | Connected = 1, 10 | Disconnected = 2 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/AeronReservedValue.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Abc.Aeron.ClientServer 6 | { 7 | [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 8)] 8 | [SuppressMessage("ReSharper", "PrivateFieldCanBeConvertedToLocalVariable")] 9 | internal readonly struct AeronReservedValue 10 | { 11 | public readonly byte ProtocolVersion; 12 | public readonly AeronMessageType MessageType; 13 | private readonly short _reservedShort; 14 | public readonly int SessionId; 15 | 16 | public AeronReservedValue(byte protocolVersion, AeronMessageType messageType, int sessionId) 17 | { 18 | ProtocolVersion = protocolVersion; 19 | MessageType = messageType; 20 | _reservedShort = default; 21 | SessionId = sessionId; 22 | } 23 | 24 | public static unsafe explicit operator long(AeronReservedValue value) 25 | { 26 | return Unsafe.ReadUnaligned(&value); 27 | } 28 | 29 | public static unsafe explicit operator AeronReservedValue(long value) 30 | { 31 | return Unsafe.ReadUnaligned(&value); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/AeronResultType.cs: -------------------------------------------------------------------------------- 1 | namespace Abc.Aeron.ClientServer 2 | { 3 | public enum AeronResultType 4 | { 5 | Error, 6 | ShouldRetry, 7 | Success 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/AeronServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Diagnostics; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Runtime.CompilerServices; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Adaptive.Aeron; 11 | using Adaptive.Aeron.Driver.Native; 12 | using Adaptive.Aeron.Exceptions; 13 | using Adaptive.Aeron.LogBuffer; 14 | using Adaptive.Agrona; 15 | using Adaptive.Agrona.Concurrent; 16 | using log4net; 17 | using ProtoBuf; 18 | 19 | namespace Abc.Aeron.ClientServer 20 | { 21 | public delegate void AeronServerMessageReceivedHandler(long identity, ReadOnlySpan message); 22 | 23 | public class AeronServer : IDisposable 24 | { 25 | private const int _frameCountLimit = 16384; 26 | internal const int ServerStreamId = 1; 27 | 28 | private static readonly ILog _log = LogManager.GetLogger(typeof(AeronServer)); 29 | private static readonly ILog _driverLog = LogManager.GetLogger(typeof(AeronDriver)); 30 | 31 | private readonly ClientServerConfig _config; 32 | private readonly AeronDriver.DriverContext _driverContext; 33 | private readonly Adaptive.Aeron.Aeron.Context _clientContext; 34 | private readonly AeronDriver _driver; 35 | private readonly Adaptive.Aeron.Aeron _client; 36 | 37 | private readonly IIdleStrategy _publicationIdleStrategy; 38 | private readonly Subscription _subscription; 39 | 40 | private readonly ConcurrentDictionary _clientSessions = new ConcurrentDictionary(); 41 | 42 | private volatile bool _isRunning; 43 | private volatile bool _isTerminating; 44 | private Thread? _pollThread; 45 | 46 | public AeronServer(int serverPort, ClientServerConfig config) 47 | { 48 | _config = config; 49 | _driverContext = config.ToDriverContext(); 50 | _clientContext = config.ToClientContext(); 51 | 52 | _driverContext 53 | .LoggerInfo(_driverLog.Info) 54 | .LoggerWarning(_driverLog.Warn) 55 | .LoggerWarning(_driverLog.Error); 56 | 57 | _driver = AeronDriver.Start(_driverContext); 58 | 59 | _clientContext 60 | .ErrorHandler(OnError) 61 | .AvailableImageHandler(ConnectionOnImageAvailable) 62 | .UnavailableImageHandler(ConnectionOnImageUnavailable); 63 | 64 | _client = Adaptive.Aeron.Aeron.Connect(_clientContext); 65 | 66 | _publicationIdleStrategy = _config.ClientIdleStrategy.GetClientIdleStrategy(); 67 | 68 | _subscription = _client.AddSubscription($"aeron:udp?endpoint=0.0.0.0:{serverPort}", ServerStreamId); 69 | } 70 | 71 | private void OnError(Exception exception) 72 | { 73 | _driverLog.Error("Aeron connection error", exception); 74 | 75 | if (_client.IsClosed) 76 | return; 77 | 78 | switch (exception) 79 | { 80 | case AeronException _: 81 | case AgentTerminationException _: 82 | _log.Error("Unrecoverable Media Driver error"); 83 | ConnectionOnTerminatedUnexpectedly(); 84 | break; 85 | } 86 | } 87 | 88 | public event AeronServerMessageReceivedHandler? MessageReceived; 89 | 90 | public event Action? ClientConnected; 91 | public event Action? ClientDisconnected; 92 | 93 | public event Action? TerminatedUnexpectedly; 94 | 95 | public bool IsRunning => _isRunning; 96 | 97 | private static void ConnectionOnImageAvailable(Image image) 98 | { 99 | if (_log.IsDebugEnabled) 100 | { 101 | var subscription = image.Subscription; 102 | _log.Debug( 103 | $"Available image on {subscription.Channel} streamId={subscription.StreamId:D} sessionId={image.SessionId:D} from {image.SourceIdentity}"); 104 | } 105 | } 106 | 107 | private void ConnectionOnImageUnavailable(Image image) 108 | { 109 | var subscription = image.Subscription; 110 | 111 | var peer = GetSession(image); 112 | 113 | if (peer != null) 114 | DisconnectPeer(peer.Publication.SessionId); 115 | 116 | Debug.Assert(subscription.StreamId == ServerStreamId, "subscription.StreamId == ServerStreamId"); 117 | 118 | if (_log.IsDebugEnabled) 119 | _log.Debug( 120 | $"Unavailable image on {subscription.Channel} streamId={subscription.StreamId:D} sessionId={image.SessionId:D} from {image.SourceIdentity}"); 121 | } 122 | 123 | private ClientSession? GetSession(Image image) 124 | { 125 | var peer = _clientSessions.SingleOrDefault(kvp => kvp.Value.Image == image); 126 | return peer.Value; 127 | } 128 | 129 | private bool TryAddSession(ClientSession session) 130 | { 131 | if (GetSession(session.Image) != null) 132 | { 133 | _log.Warn( 134 | $"Tried to add a session with existing source identity [{session.Image.SourceIdentity}] and session id [{session.Image.SessionId}]"); 135 | return false; 136 | } 137 | 138 | return _clientSessions.TryAdd(session.Publication.SessionId, session); 139 | } 140 | 141 | [MethodImpl(MethodImplOptions.NoInlining)] 142 | public void DisconnectPeer(int publicationSessionId) 143 | { 144 | if (_clientSessions.TryRemove(publicationSessionId, out var session)) 145 | { 146 | _log.Info($"Disconnected client: {session}"); 147 | ClientDisconnected?.Invoke(session.ToIdentity()); 148 | 149 | // re-entry is forbidden from callback 150 | Task.Run(session.Dispose); 151 | } 152 | } 153 | 154 | public void Start() 155 | { 156 | if (_isRunning) 157 | return; 158 | 159 | _pollThread = new Thread(PollThread) 160 | { 161 | IsBackground = true, Name = "AeronServer Poll Thread" 162 | }; 163 | _pollThread.Start(); 164 | 165 | _isRunning = true; 166 | } 167 | 168 | private void PollThread() 169 | { 170 | var idleStrategy = _config.ClientIdleStrategy.GetClientIdleStrategy(); 171 | var fragmentHandler = new FragmentAssembler(HandlerHelper.ToFragmentHandler(SubscriptionHandler)); 172 | 173 | while (_isRunning && !_isTerminating) 174 | { 175 | idleStrategy.Idle(_subscription.Poll(fragmentHandler, _frameCountLimit)); 176 | } 177 | } 178 | 179 | private void SubscriptionHandler(IDirectBuffer buffer, int offset, int length, Header header) 180 | { 181 | if (!_isRunning) 182 | return; 183 | 184 | var reservedValue = (AeronReservedValue)header.ReservedValue; 185 | 186 | if (reservedValue.ProtocolVersion != Utils.CurrentProtocolVersion) 187 | { 188 | _log.Error( 189 | $"Received message with unsupported protocol version: {reservedValue.ProtocolVersion} from {(header.Context as Image)?.SourceIdentity}, sessionId={header.SessionId}, ignoring"); 190 | return; 191 | } 192 | 193 | if (reservedValue.MessageType == AeronMessageType.Data) 194 | DataHandler(buffer, offset, length, reservedValue.SessionId); 195 | else 196 | ConnectionHandler(buffer, offset, length, header); 197 | } 198 | 199 | private unsafe void ConnectionHandler(IDirectBuffer buffer, int offset, int length, Header header) 200 | { 201 | var reservedValue = (AeronReservedValue)header.ReservedValue; 202 | var messageType = reservedValue.MessageType; 203 | 204 | if (messageType == AeronMessageType.Connected) 205 | { 206 | if (!(header.Context is Image image)) 207 | { 208 | _log.Warn("Received connection without image, ignoring"); 209 | return; 210 | } 211 | 212 | var handshake = Serializer.DeserializeWithLengthPrefix( 213 | new UnmanagedMemoryStream((byte*)buffer.BufferPointer + offset, length), 214 | PrefixStyle.Base128); 215 | 216 | var publication = _client.AddPublication(handshake.Channel, handshake.StreamId); 217 | var session = new ClientSession(this, publication, image); 218 | 219 | if (TryAddSession(session)) 220 | { 221 | _log.Info($"New client session: {session}"); 222 | 223 | // Do not block the polling thread waiting for the other side to connect 224 | Task.Run(() => InitializeSession(session)); 225 | } 226 | else 227 | { 228 | _log.Warn($"Duplicate client session: {session}"); 229 | session.Dispose(); 230 | } 231 | } 232 | else if (messageType == AeronMessageType.Disconnected) 233 | { 234 | DisconnectPeer(reservedValue.SessionId); 235 | } 236 | } 237 | 238 | [SuppressMessage("ReSharper", "AccessToDisposedClosure")] 239 | private void InitializeSession(ClientSession session) 240 | { 241 | try 242 | { 243 | session.Buffer.Release(); 244 | 245 | var spinWait = new SpinWait(); 246 | var stopwatch = Stopwatch.StartNew(); 247 | 248 | while (true) 249 | { 250 | var errorCode = session.Publication.Offer(session.Buffer, 251 | 0, 252 | 0, 253 | (buffer, offset, length) => (long)new AeronReservedValue(Utils.CurrentProtocolVersion, 254 | AeronMessageType.Connected, 255 | session.Publication.SessionId)); 256 | 257 | if (errorCode == Publication.NOT_CONNECTED) 258 | { 259 | // This will happen as we just created the publication - we need to wait for Aeron to do its stuff 260 | 261 | if (stopwatch.Elapsed > TimeSpan.FromSeconds(30)) 262 | throw new InvalidOperationException( 263 | $"Timed out while waiting to send handshake to {session}"); 264 | 265 | spinWait.SpinOnce(); 266 | continue; 267 | } 268 | 269 | var result = Utils.InterpretPublicationOfferResult(errorCode); 270 | 271 | if (result == AeronResultType.Success) 272 | break; 273 | 274 | if (result == AeronResultType.ShouldRetry) 275 | { 276 | spinWait.SpinOnce(); 277 | continue; 278 | } 279 | 280 | throw new InvalidOperationException($"Failed to send handshake to {session}"); 281 | } 282 | 283 | session.Buffer.Release(); 284 | 285 | _log.Info($"Connected client: {session}"); 286 | ClientConnected?.Invoke(session.ToIdentity()); 287 | } 288 | catch (Exception ex) 289 | { 290 | _log.Error($"Session initialization failed for {session}: {ex}"); 291 | _clientSessions.TryRemove(session.Publication.SessionId, out _); 292 | session.Dispose(); 293 | } 294 | } 295 | 296 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 297 | private unsafe void DataHandler(IDirectBuffer buffer, int offset, int length, int publicationSessionId) 298 | { 299 | if (_clientSessions.ContainsKey(publicationSessionId)) 300 | { 301 | MessageReceived?.Invoke(publicationSessionId, 302 | new ReadOnlySpan((byte*)buffer.BufferPointer + offset, length)); 303 | } 304 | else 305 | { 306 | _log.Warn($"Received message from unknown peer. Publication SessionId: {publicationSessionId}"); 307 | } 308 | } 309 | 310 | public void Send(int identity, ReadOnlySpan message) 311 | { 312 | var publicationSessionId = identity; 313 | 314 | if (_clientSessions.TryGetValue(publicationSessionId, out var session)) 315 | session.Send(message); 316 | } 317 | 318 | private void ConnectionOnTerminatedUnexpectedly() 319 | { 320 | _isTerminating = true; 321 | 322 | try 323 | { 324 | foreach (var kvp in _clientSessions) 325 | { 326 | var session = kvp.Value; 327 | try 328 | { 329 | _log.Info($"Disconnected client due to unhandled error: {session}"); 330 | ClientDisconnected?.Invoke(session.ToIdentity()); 331 | } 332 | catch (Exception ex) 333 | { 334 | _log.Error(ex); 335 | } 336 | } 337 | } 338 | finally 339 | { 340 | TerminatedUnexpectedly?.Invoke(); 341 | DisposeImpl(false); 342 | } 343 | } 344 | 345 | public void Dispose() 346 | => DisposeImpl(true); 347 | 348 | private void DisposeImpl(bool sendDisconnectMessage) 349 | { 350 | if (!_isRunning) 351 | return; 352 | 353 | _isRunning = false; 354 | 355 | foreach (var kvp in _clientSessions) 356 | { 357 | var session = kvp.Value; 358 | if (sendDisconnectMessage) 359 | { 360 | while (true) 361 | { 362 | if (!_driver.IsDriverRunning) 363 | break; 364 | 365 | var result = Utils.InterpretPublicationOfferResult( 366 | session.Publication.Offer(session.Buffer, 367 | 0, 368 | 0, 369 | (buffer, offset, length) => 370 | (long)new AeronReservedValue(Utils.CurrentProtocolVersion, 371 | AeronMessageType.Disconnected, 372 | session.Publication.SessionId))); 373 | 374 | if (result == AeronResultType.ShouldRetry) 375 | { 376 | Thread.SpinWait(1); 377 | continue; 378 | } 379 | 380 | break; 381 | } 382 | } 383 | 384 | session.Dispose(); 385 | } 386 | 387 | _clientSessions.Clear(); 388 | 389 | if (_pollThread != null && !_pollThread.Join(TimeSpan.FromSeconds(30))) 390 | _log.Warn("Could not join poll thread"); 391 | 392 | // linger for disconnect messages and pool thread 393 | Thread.Sleep(500); 394 | 395 | _subscription.Dispose(); 396 | try 397 | { 398 | _client.Dispose(); 399 | _driver.Dispose(); // last 400 | } 401 | catch(AeronDriver.MediaDriverException) 402 | { 403 | try 404 | { 405 | if (_config.DirDeleteOnShutdown && Directory.Exists(_config.Dir)) 406 | Directory.Delete(_config.Dir, true); 407 | } 408 | catch(Exception ex) 409 | { 410 | _log.Warn($"Cannot delete server dir [{_config.Dir}] on shutdown:\n{ex}"); 411 | } 412 | } 413 | } 414 | 415 | private class ClientSession : IDisposable 416 | { 417 | private readonly AeronServer _server; 418 | private readonly ReservedValueSupplier _dataReservedValueSupplier; 419 | 420 | public Image Image { get; } 421 | public Publication Publication { get; } 422 | 423 | public UnsafeBuffer Buffer { get; } = new UnsafeBuffer(); 424 | 425 | public ClientSession(AeronServer server, Publication publication, Image image) 426 | { 427 | _server = server; 428 | Image = image; 429 | Publication = publication; 430 | 431 | var dataReservedValue = (long)new AeronReservedValue(Utils.CurrentProtocolVersion, 432 | AeronMessageType.Data, 433 | Publication.SessionId); 434 | _dataReservedValueSupplier = (buffer, offset, length) => dataReservedValue; 435 | } 436 | 437 | public void Dispose() 438 | { 439 | Publication.Dispose(); 440 | Buffer.Dispose(); 441 | } 442 | 443 | public int ToIdentity() => Publication.SessionId; 444 | 445 | public unsafe void Send(ReadOnlySpan message) 446 | { 447 | fixed (byte* ptr = message) 448 | { 449 | Buffer.Wrap(ptr, message.Length); 450 | 451 | if (Publication.Offer(Buffer, 0, message.Length, _dataReservedValueSupplier) < 0) 452 | { 453 | _server._publicationIdleStrategy.Reset(); 454 | 455 | while (true) 456 | { 457 | var result = Utils.InterpretPublicationOfferResult( 458 | Publication.Offer(Buffer, 0, message.Length, _dataReservedValueSupplier)); 459 | 460 | if (result == AeronResultType.Success) 461 | break; 462 | 463 | if (result == AeronResultType.ShouldRetry) 464 | { 465 | _server._publicationIdleStrategy.Idle(); 466 | continue; 467 | } 468 | 469 | _server.DisconnectPeer(Publication.SessionId); 470 | return; 471 | } 472 | } 473 | 474 | Buffer.Release(); 475 | } 476 | } 477 | 478 | public override string ToString() => 479 | $"{Image.SourceIdentity}, Image: {Image.SessionId}, Publication: {Publication.SessionId}, StreamId: {Publication.StreamId}"; 480 | } 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/ClientServerConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | using Adaptive.Aeron.Driver.Native; 5 | 6 | namespace Abc.Aeron.ClientServer 7 | { 8 | [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] 9 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] 10 | public class ClientServerConfig 11 | { 12 | /// 13 | /// Offset (in bytes) of client stream id counter . 14 | /// 15 | internal const int ClientStreamIdCounterOffset = 0; 16 | 17 | public ClientServerConfig(string dir) 18 | { 19 | if (string.IsNullOrWhiteSpace(dir)) 20 | throw new ArgumentException("Aeron directory must be a valid path."); 21 | Dir = Path.GetFullPath(dir); 22 | } 23 | 24 | public string Dir { get; } 25 | public int DriverTimeout { get; set; } = 10_000; 26 | 27 | public bool UseActiveDriverIfPresent { get; set; } = false; 28 | 29 | public bool DirDeleteOnStart { get; set; } = true; 30 | 31 | public bool DirDeleteOnShutdown { get; set; } = true; 32 | public bool PreTouchMappedMemory { get; set; } = true; 33 | 34 | // see low-latency config here https://github.com/real-logic/aeron/blob/master/aeron-driver/src/main/resources/low-latency.properties 35 | 36 | /// 37 | /// The length in bytes of a publication buffer to hold a term of messages. It must be a power of 2 and be in the range of 64KB to 1GB. 38 | /// 39 | public int TermBufferLength { get; set; } = 16 * 1024 * 1024; 40 | 41 | public int SocketSoSndBuf { get; set; } = 1 * 1024 * 1024; 42 | public int SocketSoRcvBuf { get; set; } = 1 * 1024 * 1024; 43 | public int RcvInitialWindowLength { get; set; } = 128 * 1024; 44 | public AeronThreadingModeEnum ThreadingMode { get; set; } = AeronThreadingModeEnum.AeronThreadingModeSharedNetwork; 45 | public DriverIdleStrategy SenderIdleStrategy { get; set; } = DriverIdleStrategy.YIELDING; 46 | public DriverIdleStrategy ReceiverIdleStrategy { get; set; } = DriverIdleStrategy.YIELDING; 47 | public DriverIdleStrategy ConductorIdleStrategy { get; set; } = DriverIdleStrategy.SLEEPING; 48 | public DriverIdleStrategy SharedNetworkIdleStrategy { get; set; } = DriverIdleStrategy.SPINNING; 49 | public DriverIdleStrategy SharedIdleStrategy { get; set; } = DriverIdleStrategy.SPINNING; 50 | public DriverIdleStrategy ClientIdleStrategy { get; set; } = DriverIdleStrategy.YIELDING; 51 | 52 | public string Code 53 | { 54 | get 55 | { 56 | var code = ThreadingMode switch 57 | { 58 | AeronThreadingModeEnum.AeronThreadingModeDedicated =>$"acd{SenderIdleStrategy.Name[0].ToString()}", 59 | AeronThreadingModeEnum.AeronThreadingModeSharedNetwork =>$"acn{SharedNetworkIdleStrategy.Name[0].ToString()}", 60 | AeronThreadingModeEnum.AeronThreadingModeShared =>$"acs{SharedIdleStrategy.Name[0].ToString()}", 61 | _ => throw new ArgumentOutOfRangeException() 62 | }; 63 | 64 | return code; 65 | } 66 | } 67 | 68 | public static ClientServerConfig DedicatedYielding(string directory) 69 | { 70 | return new ClientServerConfig(directory) 71 | { 72 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeDedicated, 73 | SenderIdleStrategy = DriverIdleStrategy.YIELDING, 74 | ReceiverIdleStrategy = DriverIdleStrategy.YIELDING, 75 | ConductorIdleStrategy = DriverIdleStrategy.SLEEPING, 76 | ClientIdleStrategy = DriverIdleStrategy.YIELDING 77 | }; 78 | } 79 | 80 | public static ClientServerConfig DedicatedSpinning(string directory) 81 | { 82 | return new ClientServerConfig(directory) 83 | { 84 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeDedicated, 85 | SenderIdleStrategy = DriverIdleStrategy.SPINNING, 86 | ReceiverIdleStrategy = DriverIdleStrategy.SPINNING, 87 | ConductorIdleStrategy = DriverIdleStrategy.YIELDING, 88 | ClientIdleStrategy = DriverIdleStrategy.SPINNING 89 | }; 90 | } 91 | 92 | public static ClientServerConfig DedicatedNoOp(string directory) 93 | { 94 | return new ClientServerConfig(directory) 95 | { 96 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeDedicated, 97 | SenderIdleStrategy = DriverIdleStrategy.NOOP, 98 | ReceiverIdleStrategy = DriverIdleStrategy.NOOP, 99 | ConductorIdleStrategy = DriverIdleStrategy.SPINNING, 100 | ClientIdleStrategy = DriverIdleStrategy.NOOP 101 | }; 102 | } 103 | 104 | public static ClientServerConfig SharedNetworkSleeping(string directory) 105 | { 106 | return new ClientServerConfig(directory) 107 | { 108 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeSharedNetwork, 109 | SenderIdleStrategy = DriverIdleStrategy.SLEEPING, 110 | ReceiverIdleStrategy = DriverIdleStrategy.SLEEPING, 111 | SharedNetworkIdleStrategy = DriverIdleStrategy.SLEEPING, 112 | ConductorIdleStrategy = DriverIdleStrategy.SLEEPING, 113 | ClientIdleStrategy = DriverIdleStrategy.SLEEPING 114 | }; 115 | } 116 | 117 | public static ClientServerConfig SharedNetworkBackoff(string directory) 118 | { 119 | return new ClientServerConfig(directory) 120 | { 121 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeSharedNetwork, 122 | SenderIdleStrategy = DriverIdleStrategy.BACKOFF, 123 | ReceiverIdleStrategy = DriverIdleStrategy.BACKOFF, 124 | SharedNetworkIdleStrategy = DriverIdleStrategy.BACKOFF, 125 | ConductorIdleStrategy = DriverIdleStrategy.SLEEPING, 126 | ClientIdleStrategy = DriverIdleStrategy.BACKOFF 127 | }; 128 | } 129 | 130 | public static ClientServerConfig SharedNetworkYielding(string directory) 131 | { 132 | return new ClientServerConfig(directory) 133 | { 134 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeSharedNetwork, 135 | SenderIdleStrategy = DriverIdleStrategy.YIELDING, 136 | ReceiverIdleStrategy = DriverIdleStrategy.YIELDING, 137 | SharedNetworkIdleStrategy = DriverIdleStrategy.YIELDING, 138 | ConductorIdleStrategy = DriverIdleStrategy.SLEEPING, 139 | ClientIdleStrategy = DriverIdleStrategy.YIELDING 140 | }; 141 | } 142 | 143 | public static ClientServerConfig SharedNetworkSpinning(string directory) 144 | { 145 | return new ClientServerConfig(directory) 146 | { 147 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeSharedNetwork, 148 | SharedNetworkIdleStrategy = DriverIdleStrategy.SPINNING, 149 | ConductorIdleStrategy = DriverIdleStrategy.YIELDING, 150 | ClientIdleStrategy = DriverIdleStrategy.SPINNING 151 | }; 152 | } 153 | 154 | public static ClientServerConfig SharedNetworkNoOp(string directory) 155 | { 156 | return new ClientServerConfig(directory) 157 | { 158 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeSharedNetwork, 159 | SharedNetworkIdleStrategy = DriverIdleStrategy.NOOP, 160 | ConductorIdleStrategy = DriverIdleStrategy.SPINNING, 161 | ClientIdleStrategy = DriverIdleStrategy.NOOP 162 | }; 163 | } 164 | 165 | public static ClientServerConfig SharedYielding(string directory) 166 | { 167 | return new ClientServerConfig(directory) 168 | { 169 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeShared, 170 | SharedIdleStrategy = DriverIdleStrategy.YIELDING, 171 | ClientIdleStrategy = DriverIdleStrategy.YIELDING 172 | }; 173 | } 174 | 175 | public static ClientServerConfig SharedSpinning(string directory) 176 | { 177 | return new ClientServerConfig(directory) 178 | { 179 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeShared, 180 | SharedIdleStrategy = DriverIdleStrategy.SPINNING, 181 | ClientIdleStrategy = DriverIdleStrategy.SPINNING 182 | }; 183 | } 184 | 185 | public static ClientServerConfig SharedNoOp(string directory) 186 | { 187 | return new ClientServerConfig(directory) 188 | { 189 | ThreadingMode = AeronThreadingModeEnum.AeronThreadingModeShared, 190 | SharedIdleStrategy = DriverIdleStrategy.NOOP, 191 | ClientIdleStrategy = DriverIdleStrategy.NOOP 192 | }; 193 | } 194 | 195 | internal AeronDriver.DriverContext ToDriverContext() 196 | { 197 | return new AeronDriver.DriverContext() 198 | .AeronDirectoryName(Dir) 199 | .DriverTimeoutMs(DriverTimeout) 200 | .UseActiveDriverIfPresent(UseActiveDriverIfPresent) 201 | .DirDeleteOnStart(DirDeleteOnStart) 202 | .DirDeleteOnShutdown(DirDeleteOnShutdown) 203 | .TermBufferLength(TermBufferLength) 204 | .SocketSndbufLength(SocketSoSndBuf) 205 | .SocketRcvbufLength(SocketSoRcvBuf) 206 | .InitialWindowLength(RcvInitialWindowLength) 207 | .ThreadingMode(ThreadingMode) 208 | .SenderIdleStrategy(SenderIdleStrategy) 209 | .ReceiverIdleStrategy(ReceiverIdleStrategy) 210 | .ConductorIdleStrategy(ConductorIdleStrategy) 211 | .SharedNetworkIdleStrategy(SharedNetworkIdleStrategy) 212 | .SenderIdleStrategy(SharedIdleStrategy); 213 | 214 | // Note that ClientIdleStrategy is not for the driver, but for Client/Server 215 | } 216 | 217 | internal Adaptive.Aeron.Aeron.Context ToClientContext() 218 | { 219 | return new Adaptive.Aeron.Aeron.Context() 220 | .AeronDirectoryName(Dir) 221 | .DriverTimeoutMs(DriverTimeout) 222 | .PreTouchMappedMemory(PreTouchMappedMemory); 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Adaptive.Aeron.Driver.Native; 3 | using Adaptive.Agrona.Concurrent; 4 | 5 | namespace Abc.Aeron.ClientServer 6 | { 7 | internal static class Extensions 8 | { 9 | public static unsafe void Release(this UnsafeBuffer buffer) 10 | => buffer.Wrap((byte*)0, 0); 11 | 12 | public static IIdleStrategy GetClientIdleStrategy(this DriverIdleStrategy driverIdleStrategy) 13 | { 14 | if (driverIdleStrategy.Name == DriverIdleStrategy.SLEEPING.Name) 15 | return new SleepingIdleStrategy(1); 16 | 17 | if (driverIdleStrategy.Name == DriverIdleStrategy.YIELDING.Name) 18 | return new YieldingIdleStrategy(); 19 | 20 | if (driverIdleStrategy.Name == DriverIdleStrategy.SPINNING.Name) 21 | return new BusySpinIdleStrategy(); 22 | 23 | if (driverIdleStrategy.Name == DriverIdleStrategy.NOOP.Name) 24 | return new NoOpIdleStrategy(); 25 | 26 | if (driverIdleStrategy.Name == DriverIdleStrategy.BACKOFF.Name) 27 | return new BackoffIdleStrategy(1, 10, 1, 1); 28 | 29 | throw new ArgumentException(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/MachineCounters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.ConstrainedExecution; 5 | using System.Threading; 6 | using Adaptive.Agrona; 7 | using Adaptive.Agrona.Util; 8 | 9 | namespace Abc.Aeron.ClientServer 10 | { 11 | /// 12 | /// Persistent counters per machine (as opposed to Aeron's built-in per driver counters) 13 | /// 14 | public class MachineCounters : CriticalFinalizerObject, IDisposable 15 | { 16 | public static readonly MachineCounters Instance = new MachineCounters(); 17 | 18 | private const string ClientCountersFileName = "client.counters"; 19 | 20 | /// 21 | /// Offset (in bytes) of client stream id counter. 22 | /// 23 | internal const int ClientStreamIdCounterOffset = 0; 24 | 25 | private MappedByteBuffer _clientCounters; 26 | 27 | private MachineCounters() 28 | { 29 | // Client stream id should be unique per machine, not per media driver. 30 | // Temp is ok, we only need to avoid same stream id among concurrently 31 | // running clients (within 10 seconds), not forever. If a machine 32 | // restarts and cleans temp there are no other running client and 33 | // we could start from 0 again. 34 | 35 | var dirPath = 36 | Path.Combine(Path.GetTempPath(), "aeron_client_counters"); // any name, but do not start with 'aeron-' 37 | Directory.CreateDirectory(dirPath); 38 | var counterFile = Path.Combine(dirPath, ClientCountersFileName); 39 | _clientCounters = IoUtil.MapNewOrExistingFile(new FileInfo(counterFile), 4096); 40 | } 41 | 42 | public unsafe int GetNewClientStreamId() 43 | { 44 | var id = Interlocked.Increment( 45 | ref Unsafe.AsRef((void*) IntPtr.Add(_clientCounters.Pointer, ClientStreamIdCounterOffset)) 46 | ); 47 | _clientCounters.Flush(); 48 | return id; 49 | } 50 | 51 | private void DoDispose() 52 | { 53 | var counters = Interlocked.Exchange(ref _clientCounters, null!); 54 | if (counters == null!) 55 | return; 56 | counters.Dispose(); 57 | } 58 | 59 | public void Dispose() 60 | { 61 | DoDispose(); 62 | GC.SuppressFinalize(this); 63 | } 64 | 65 | ~MachineCounters() => DoDispose(); 66 | } 67 | } -------------------------------------------------------------------------------- /src/Aeron.ClientServer/RateLimiter.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | 5 | namespace Abc.Aeron.ClientServer 6 | { 7 | public class RateLimiter 8 | { 9 | public const int CHUNK_SIZE = 64 * 1024; 10 | private const int GROW_FACTOR = 4; 11 | 12 | public double BwLimitBytes { get; } 13 | 14 | private static readonly double _frequency = Stopwatch.Frequency; 15 | 16 | // long is enough for ~22 years at 100 GBits/sec 17 | private long _head; 18 | private long _tail; 19 | private long _tailTicks; 20 | 21 | public RateLimiter(long bandwidthLimitMegabits = 1024) 22 | { 23 | BwLimitBytes = 1024.0 * 1024 * bandwidthLimitMegabits / 8; 24 | _tailTicks = Stopwatch.GetTimestamp(); 25 | } 26 | 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | public void ApplyRateLimit(int bufferSize) 29 | { 30 | ApplyRateLimit(bufferSize, out _, out _); 31 | } 32 | 33 | public void ApplyRateLimit(int bufferSize, out long newHead, out long newTicks) 34 | { 35 | var tail = Volatile.Read(ref _tail); 36 | var tailTicks = Volatile.Read(ref _tailTicks); 37 | 38 | newHead = Interlocked.Add(ref _head, bufferSize); 39 | 40 | newTicks = Stopwatch.GetTimestamp(); 41 | 42 | var initialTicks = tailTicks; 43 | 44 | while (true) 45 | { 46 | var writtenBytes = newHead - tail; 47 | var secondsElapsed = (newTicks - tailTicks) / _frequency; 48 | var bw = (writtenBytes - (CHUNK_SIZE - (CHUNK_SIZE >> GROW_FACTOR))) / secondsElapsed; 49 | 50 | if (bw <= BwLimitBytes) 51 | { 52 | newTicks = Stopwatch.GetTimestamp(); 53 | break; 54 | } 55 | 56 | // just wait, on every iteration GetTimestamp increases and calculated BW decreases 57 | Thread.SpinWait(1); 58 | 59 | tail = Volatile.Read(ref _tail); 60 | tailTicks = Volatile.Read(ref _tailTicks); 61 | newTicks = Stopwatch.GetTimestamp(); 62 | } 63 | 64 | // Only one thread could succeed with CAS 65 | // First, we update tail ticks, making instant BW higher, then we update the tail. 66 | if (// initialTicks < newTicks && 67 | newHead - Volatile.Read(ref _tail) >= CHUNK_SIZE 68 | && initialTicks == Interlocked.CompareExchange(ref _tailTicks, newTicks, initialTicks)) 69 | { 70 | Interlocked.Add(ref _tail, CHUNK_SIZE >> GROW_FACTOR); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Aeron.ClientServer/UdpUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | 6 | namespace Abc.Aeron.ClientServer 7 | { 8 | public static class UdpUtils 9 | { 10 | public static int GetRandomUnusedPort() 11 | { 12 | using var udpc = new UdpClient(0); 13 | return ((IPEndPoint)udpc.Client.LocalEndPoint).Port; 14 | } 15 | 16 | public static IReadOnlyList GetRandomUnusedPorts(int count) 17 | { 18 | var ports = new int[count]; 19 | var listeners = new UdpClient[count]; 20 | 21 | for (var i = 0; i < count; ++i) 22 | { 23 | var listener = new UdpClient(0); 24 | listeners[i] = listener; 25 | ports[i] = ((IPEndPoint)listener.Client.LocalEndPoint).Port; 26 | } 27 | 28 | for (var i = 0; i < count; ++i) 29 | listeners[i].Dispose(); 30 | 31 | return ports; 32 | } 33 | 34 | public static bool IsPortUnused(int port) 35 | { 36 | try 37 | { 38 | using var _ = new UdpClient(port); 39 | } 40 | catch (Exception) 41 | { 42 | return false; 43 | } 44 | 45 | return true; 46 | } 47 | 48 | /// 49 | /// Get local IP address that OS chooses for the remote destination. 50 | /// This should be more reliable than iterating over local interfaces 51 | /// and trying to guess the right one. 52 | /// 53 | public static string GetLocalIPAddress(string serverHost, int serverPort) 54 | { 55 | using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0); 56 | socket.Connect(serverHost, serverPort); 57 | if (socket.LocalEndPoint is IPEndPoint endPoint) 58 | return endPoint.Address.ToString(); 59 | throw new InvalidOperationException("socket.LocalEndPoint is not IPEndPoint"); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Aeron.ClientServer/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using Adaptive.Aeron; 6 | using log4net; 7 | 8 | namespace Abc.Aeron.ClientServer 9 | { 10 | public static class Utils 11 | { 12 | private static readonly ILog _log = LogManager.GetLogger(typeof(Utils)); 13 | 14 | public const byte CurrentProtocolVersion = 1; 15 | 16 | public const string IpcChannel = "aeron:ipc"; 17 | 18 | public static string RemoteChannel(string host, int port) 19 | => $"aeron:udp?endpoint={host}:{port}"; 20 | 21 | public static string GroupIdToPath(string groupId) 22 | { 23 | if (string.IsNullOrEmpty(groupId)) 24 | throw new ArgumentException("Empty group id"); 25 | 26 | return Path.Combine(Path.GetTempPath(), $"Aeron-{groupId}"); 27 | } 28 | 29 | public static AeronResultType InterpretPublicationOfferResult(long errorCode) 30 | { 31 | if (errorCode >= 0) 32 | return AeronResultType.Success; 33 | 34 | switch (errorCode) 35 | { 36 | case Publication.BACK_PRESSURED: 37 | case Publication.ADMIN_ACTION: 38 | return AeronResultType.ShouldRetry; 39 | 40 | case Publication.NOT_CONNECTED: 41 | _log.Error("Trying to send to an unconnected publication"); 42 | return AeronResultType.Error; 43 | 44 | case Publication.CLOSED: 45 | _log.Error("Trying to send to a closed publication"); 46 | return AeronResultType.Error; 47 | 48 | case Publication.MAX_POSITION_EXCEEDED: 49 | _log.Error("Max position exceeded"); 50 | return AeronResultType.Error; 51 | 52 | default: 53 | _log.Error($"Unknown error code: {errorCode}"); 54 | return AeronResultType.Error; 55 | } 56 | } 57 | 58 | /// 59 | /// Get local IP address that OS chooses for the remote destination. 60 | /// This should be more reliable than iterating over local interfaces 61 | /// and trying to guess the right one. 62 | /// 63 | public static string GetLocalIPAddress(string serverHost, int serverPort) 64 | { 65 | using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0); 66 | socket.Connect(serverHost, serverPort); 67 | if (socket.LocalEndPoint is IPEndPoint endPoint) 68 | return endPoint.Address.ToString(); 69 | throw new InvalidOperationException("socket.LocalEndPoint is not IPEndPoint"); 70 | } 71 | } 72 | } 73 | --------------------------------------------------------------------------------