├── .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 |
--------------------------------------------------------------------------------