├── Miku.UnitTest
├── Protocol
│ ├── IProtocol.cs
│ ├── Ping.cs
│ └── Pong.cs
├── Lz4CompressionMiddleware.cs
├── Miku.UnitTest.csproj
├── PacketFrameMiddleware.cs
└── ServerTests.cs
├── Miku.Core
├── .AssemblyAttributes
├── MultiMemory.cs
├── INetMiddleware.cs
├── Miku.Core.csproj
├── NetBuffer.cs
├── NetServer.cs
└── NetClient.cs
├── Miku.PerformanceTest
├── Miku.PerformanceTest.csproj
├── README.md
└── Program.cs
├── LICENSE
├── Miku.sln
├── .github
└── workflows
│ └── dotnet.yml
├── README.md
└── .gitignore
/Miku.UnitTest/Protocol/IProtocol.cs:
--------------------------------------------------------------------------------
1 | using Nino.Core;
2 |
3 | namespace Miku.UnitTest.Protocol;
4 |
5 | [NinoType(false)]
6 | public interface IProtocol
7 | {
8 | }
--------------------------------------------------------------------------------
/Miku.Core/.AssemblyAttributes:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using System.Reflection;
4 | [assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
5 |
--------------------------------------------------------------------------------
/Miku.UnitTest/Protocol/Ping.cs:
--------------------------------------------------------------------------------
1 | using Nino.Core;
2 |
3 | namespace Miku.UnitTest.Protocol;
4 |
5 | [NinoType]
6 | public record Ping : IProtocol
7 | {
8 | public int Field1;
9 | public string Field2;
10 | public float Field3;
11 | public double Field4;
12 | public byte Field5;
13 | public short Field6;
14 | public long Field7;
15 | public Guid Field8;
16 | }
--------------------------------------------------------------------------------
/Miku.UnitTest/Protocol/Pong.cs:
--------------------------------------------------------------------------------
1 | using Nino.Core;
2 |
3 | namespace Miku.UnitTest.Protocol;
4 |
5 | [NinoType]
6 | public record Pong : IProtocol
7 | {
8 | public int Field1;
9 | public string Field2;
10 | public float Field3;
11 | public double Field4;
12 | public byte Field5;
13 | public short Field6;
14 | public long Field7;
15 | public Guid Field8;
16 | }
--------------------------------------------------------------------------------
/Miku.PerformanceTest/Miku.PerformanceTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Miku.Core/MultiMemory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | public readonly struct MultiMemory
4 | {
5 | public readonly Memory First;
6 | public readonly Memory Second;
7 |
8 | public MultiMemory(Memory first, Memory second)
9 | {
10 | First = first;
11 | Second = second;
12 | }
13 |
14 | public static MultiMemory Empty => new(Memory.Empty, Memory.Empty);
15 | public bool IsEmpty => First.IsEmpty && Second.IsEmpty;
16 | public int Length => First.Length + Second.Length;
17 |
18 | public void CopyTo(Span span)
19 | {
20 | if (span.Length < Length)
21 | throw new ArgumentException("Span is too small to copy the data.");
22 |
23 | First.Span.CopyTo(span);
24 | Second.Span.CopyTo(span.Slice(First.Length));
25 | }
26 | }
--------------------------------------------------------------------------------
/Miku.UnitTest/Lz4CompressionMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using K4os.Compression.LZ4;
3 | using Miku.Core;
4 |
5 | namespace Miku.UnitTest;
6 |
7 | ///
8 | /// Middleware that compresses and decompresses data using the LZ4 algorithm.
9 | ///
10 | public class Lz4CompressionMiddleware : INetMiddleware
11 | {
12 |
13 | public void ProcessSend(ReadOnlyMemory input, ArrayBufferWriter output)
14 | {
15 | LZ4Pickler.Pickle(input.Span, output);
16 | }
17 |
18 | public (bool halt, int consumedFromOrigin) ProcessReceive(ReadOnlyMemory input,
19 | ArrayBufferWriter output)
20 | {
21 | try
22 | {
23 | LZ4Pickler.Unpickle(input.Span, output);
24 | return (false, 0);
25 | }
26 | catch (Exception e)
27 | {
28 | Console.WriteLine(e);
29 | return (true, 0);
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/Miku.Core/INetMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 |
4 | namespace Miku.Core
5 | {
6 | ///
7 | /// Middleware for processing data before sending or receiving
8 | ///
9 | public interface INetMiddleware
10 | {
11 | ///
12 | /// Process data before sending
13 | ///
14 | /// Source data to be processed
15 | /// Destination buffer to write processed data to
16 | void ProcessSend(ReadOnlyMemory src, ArrayBufferWriter dst);
17 |
18 | ///
19 | /// Process data after receiving
20 | ///
21 | /// Source data to be processed
22 | /// Destination buffer to write processed data to
23 | /// Whether to halt the processing and how many bytes are consumed from the original input
24 | (bool halt, int consumedFromOrigin) ProcessReceive(ReadOnlyMemory src, ArrayBufferWriter dst);
25 | }
26 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 JasonXuDeveloper - 傑
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Miku.UnitTest/Miku.UnitTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | disable
7 | false
8 | true
9 |
10 |
11 |
12 |
13 | all
14 | runtime; build; native; contentfiles; analyzers; buildtransitive
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | all
23 | runtime; build; native; contentfiles; analyzers; buildtransitive
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Miku.UnitTest/PacketFrameMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Buffers.Binary;
3 | using Miku.Core;
4 |
5 | namespace Miku.UnitTest;
6 |
7 | ///
8 | /// Middleware for processing packets. Prevent framing due to tcp.
9 | ///
10 | public class PacketFrameMiddleware : INetMiddleware
11 | {
12 |
13 | public void ProcessSend(ReadOnlyMemory input, ArrayBufferWriter output)
14 | {
15 | var memory = output.GetMemory(input.Length + 4);
16 | // Write the length of the packet to the buffer.
17 | BinaryPrimitives.WriteUInt32LittleEndian(memory.Span, (uint)input.Length);
18 | // Copy the packet to the buffer.
19 | input.Span.CopyTo(memory.Span.Slice(4));
20 | // Set the output to the buffer.
21 | output.Advance(input.Length + 4);
22 | Console.WriteLine($"Send: {string.Join(',', output.WrittenMemory.Span.ToArray())}");
23 | }
24 |
25 | public (bool halt, int consumedFromOrigin) ProcessReceive(ReadOnlyMemory input,
26 | ArrayBufferWriter output)
27 | {
28 | // If we don't have enough data to read the length of the packet, we need to wait for more data.
29 | if (input.Length < 4)
30 | {
31 | return (true, 0);
32 | }
33 |
34 | // Read the length of the packet.
35 | var length = BinaryPrimitives.ReadUInt32LittleEndian(input.Span);
36 | // Ensure the length of the packet is valid.
37 | if (length > input.Length - 4)
38 | {
39 | return (true, 0);
40 | }
41 |
42 | Console.WriteLine($"Receive: {string.Join(',', input.ToArray())}");
43 | // Advance the input to the start of the packet.
44 | output.Write(input.Slice(4, (int)length).Span);
45 | // Set the consumed from origin to 4 + the length of the packet.
46 | return (false, 4 + (int)length);
47 | }
48 | }
--------------------------------------------------------------------------------
/Miku.Core/Miku.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | disable
5 | disable
6 | 11
7 | false
8 | 2.0.0
9 | Miku
10 | Miku
11 | JasonXuDeveloper
12 | High performance TCP server/client library written in C#
13 | JasonXuDeveloper
14 | https://github.com/JasonXuDeveloper/Miku
15 | https://github.com/JasonXuDeveloper/Miku
16 | git
17 | TCP;Networking;High-Perofrmance
18 | MIT
19 | README.md
20 | true
21 | refs/heads/main
22 | net6.0;net8.0;netstandard2.1;
23 | true
24 | true
25 | true
26 | embedded
27 |
28 |
29 |
30 |
31 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))
32 |
33 |
34 |
35 |
36 |
37 |
38 | true
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Miku.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Miku.Core", "Miku.Core\Miku.Core.csproj", "{3B44C849-FF4C-4896-9860-1B0D16798190}"
4 | EndProject
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Miku.UnitTest", "Miku.UnitTest\Miku.UnitTest.csproj", "{7534A4BC-58F7-4CA3-87EE-1E9277553FDF}"
6 | EndProject
7 |
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Miku.PerformanceTest", "Miku.PerformanceTest\Miku.PerformanceTest.csproj", "{02D12D21-290A-4615-8322-9E8F3011B626}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Debug|x64 = Debug|x64
14 | Debug|x86 = Debug|x86
15 | Release|Any CPU = Release|Any CPU
16 | Release|x64 = Release|x64
17 | Release|x86 = Release|x86
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Debug|x64.ActiveCfg = Debug|Any CPU
23 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Debug|x64.Build.0 = Debug|Any CPU
24 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Debug|x86.ActiveCfg = Debug|Any CPU
25 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Debug|x86.Build.0 = Debug|Any CPU
26 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Release|x64.ActiveCfg = Release|Any CPU
29 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Release|x64.Build.0 = Release|Any CPU
30 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Release|x86.ActiveCfg = Release|Any CPU
31 | {3B44C849-FF4C-4896-9860-1B0D16798190}.Release|x86.Build.0 = Release|Any CPU
32 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Debug|x64.ActiveCfg = Debug|Any CPU
35 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Debug|x64.Build.0 = Debug|Any CPU
36 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Debug|x86.ActiveCfg = Debug|Any CPU
37 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Debug|x86.Build.0 = Debug|Any CPU
38 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Release|Any CPU.Build.0 = Release|Any CPU
40 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Release|x64.ActiveCfg = Release|Any CPU
41 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Release|x64.Build.0 = Release|Any CPU
42 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Release|x86.ActiveCfg = Release|Any CPU
43 | {7534A4BC-58F7-4CA3-87EE-1E9277553FDF}.Release|x86.Build.0 = Release|Any CPU
44 |
45 | {02D12D21-290A-4615-8322-9E8F3011B626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46 | {02D12D21-290A-4615-8322-9E8F3011B626}.Debug|Any CPU.Build.0 = Debug|Any CPU
47 | {02D12D21-290A-4615-8322-9E8F3011B626}.Debug|x64.ActiveCfg = Debug|Any CPU
48 | {02D12D21-290A-4615-8322-9E8F3011B626}.Debug|x64.Build.0 = Debug|Any CPU
49 | {02D12D21-290A-4615-8322-9E8F3011B626}.Debug|x86.ActiveCfg = Debug|Any CPU
50 | {02D12D21-290A-4615-8322-9E8F3011B626}.Debug|x86.Build.0 = Debug|Any CPU
51 | {02D12D21-290A-4615-8322-9E8F3011B626}.Release|Any CPU.ActiveCfg = Release|Any CPU
52 | {02D12D21-290A-4615-8322-9E8F3011B626}.Release|Any CPU.Build.0 = Release|Any CPU
53 | {02D12D21-290A-4615-8322-9E8F3011B626}.Release|x64.ActiveCfg = Release|Any CPU
54 | {02D12D21-290A-4615-8322-9E8F3011B626}.Release|x64.Build.0 = Release|Any CPU
55 | {02D12D21-290A-4615-8322-9E8F3011B626}.Release|x86.ActiveCfg = Release|Any CPU
56 | {02D12D21-290A-4615-8322-9E8F3011B626}.Release|x86.Build.0 = Release|Any CPU
57 | EndGlobalSection
58 | GlobalSection(SolutionProperties) = preSolution
59 | HideSolutionNode = FALSE
60 | EndGlobalSection
61 | EndGlobal
62 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: Code Check and Release
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: Setup .NET
17 | uses: actions/setup-dotnet@v4
18 | with:
19 | dotnet-version: |
20 | 8.0.x
21 | 6.0.x
22 | 2.1.x
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal
29 |
30 | bump-version:
31 | runs-on: ubuntu-latest
32 | if: ${{ startsWith(github.event.head_commit.message, 'release v') }}
33 | needs: [ build ]
34 |
35 | steps:
36 | - name: Checkout code
37 | uses: actions/checkout@v3
38 | with:
39 | token: ${{ secrets.GITHUB_TOKEN }}
40 | fetch-depth: 0 # Ensures that we have access to the full commit history
41 |
42 | - name: Extract Version and Description
43 | id: extract_version_description
44 | run: |
45 | FULL_MESSAGE="${{ github.event.head_commit.message }}"
46 |
47 | SUMMARY=$(echo "$FULL_MESSAGE" | head -n 1)
48 | DESCRIPTION=$(echo "$FULL_MESSAGE" | tail -n +3)
49 |
50 | if [[ $SUMMARY =~ ^release\ v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
51 | VERSION="${BASH_REMATCH[1]}"
52 | else
53 | echo "Commit message does not match the pattern 'release vx.x.x'"
54 | exit 1
55 | fi
56 |
57 | echo "VERSION=$VERSION" >> $GITHUB_ENV
58 | printf "DESCRIPTION<> $GITHUB_ENV
59 |
60 | echo "Bumping version to $VERSION"
61 |
62 | - name: Run Version Bump Script
63 | run: |
64 | echo "Current directory: $(pwd)"
65 | NEW_VERSION=$VERSION
66 |
67 | # Bump the version number in Miku.Core/Miku.Core.csproj files
68 | PROJS=$(find Miku.Core -name '*.csproj')
69 |
70 | for PROJ in $PROJS; do
71 | # ver
72 | OLD_VERSION=$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' $PROJ)
73 |
74 | if [ -z "$OLD_VERSION" ]; then
75 | echo "Failed to find Version in $PROJ"
76 | exit 1
77 | fi
78 |
79 | echo "Bumping Version number in $PROJ from $OLD_VERSION to $NEW_VERSION"
80 |
81 | if [[ "$OSTYPE" == "darwin"* ]]; then
82 | # macOS
83 | sed -i "" "s/$OLD_VERSION<\/Version>/$NEW_VERSION<\/Version>/" $PROJ
84 | else
85 | # Linux
86 | sed -i "s/$OLD_VERSION<\/Version>/$NEW_VERSION<\/Version>/" $PROJ
87 | fi
88 | done
89 |
90 | - name: Configure Git
91 | run: |
92 | git config user.name "github-actions[bot]"
93 | git config user.email "github-actions[bot]@users.noreply.github.com"
94 |
95 | - name: Check for Changes
96 | id: check_changes
97 | run: |
98 | # Check if there are any changes to commit
99 | if [[ -n "$(git status --porcelain)" ]]; then
100 | echo "changes=true" >> $GITHUB_ENV
101 | else
102 | echo "changes=false" >> $GITHUB_ENV
103 | fi
104 |
105 | - name: Commit and Push Changes
106 | id: commit_version_bump # Capture this step ID to get the commit SHA
107 | if: env.changes == 'true'
108 | run: |
109 | # Commit the changes with the specified message
110 | git add .
111 | git commit -m "Bump to v$VERSION"
112 |
113 | # Push changes back to main branch
114 | git push origin main
115 | env:
116 | VERSION: ${{ env.VERSION }}
117 |
118 | - name: Get Commit SHA
119 | run: echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
120 |
121 | - name: Create GitHub Release
122 | id: create_release # Capture the release ID for uploading assets
123 | uses: actions/create-release@v1
124 | env:
125 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126 | with:
127 | tag_name: "v${{ env.VERSION }}" # Release tag, like "v1.2.3"
128 | commitish: "${{ env.COMMIT_SHA }}" # Ensures the release points to the bump commit
129 | release_name: "v${{ env.VERSION }}" # Title of the release, same as the tag
130 | body: "${{ env.DESCRIPTION }}" # Release notes from commit message
131 | draft: false # Make the release public immediately
132 | prerelease: false # Mark it as a stable release
133 |
134 | # Set up .NET Core
135 | - name: Setup .NET
136 | uses: actions/setup-dotnet@v4
137 | with:
138 | dotnet-version: |
139 | 8.0.x
140 | 6.0.x
141 | 2.1.x
142 |
143 | # Push NuGet packages
144 | - name: Push NuGet Packages
145 | run: |
146 | echo "Current directory: $(pwd)"
147 | dotnet pack Miku.Core/Miku.Core.csproj -c Release
148 | for package in $(find ./Miku.Core/bin/Release -name "*.nupkg" -print0 | sort -z -u | xargs -0 -n1 echo); do
149 | dotnet nuget push "$package" --api-key ${{ secrets.MYTOKEN }} --source https://api.nuget.org/v3/index.json --skip-duplicate
150 | done
--------------------------------------------------------------------------------
/Miku.Core/NetBuffer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.Threading;
4 |
5 | public class NetBuffer : IDisposable
6 | {
7 | private readonly byte[] _buffer;
8 | private int _head; // next write position (producer)
9 | private int _tail; // next read position (consumer)
10 | private bool _disposed;
11 |
12 | public event Action OnWarning;
13 |
14 | public NetBuffer(int capacity = 8192)
15 | {
16 | if (capacity < 2) throw new ArgumentOutOfRangeException(nameof(capacity));
17 | _buffer = ArrayPool.Shared.Rent(capacity);
18 | _head = 0;
19 | _tail = 0;
20 | }
21 |
22 | // How many bytes are available to read.
23 | public int Length
24 | {
25 | get
26 | {
27 | int head = Volatile.Read(ref _head);
28 | int tail = Volatile.Read(ref _tail);
29 | return head >= tail
30 | ? head - tail
31 | : head + _buffer.Length - tail;
32 | }
33 | }
34 |
35 | // How many bytes we can write without overwriting unread data.
36 | public int FreeSpace
37 | {
38 | get
39 | {
40 | int head = Volatile.Read(ref _head);
41 | int tail = Volatile.Read(ref _tail);
42 | // leave one slot empty so head==tail always means "empty"
43 | return tail > head
44 | ? tail - head - 1
45 | : tail + _buffer.Length - head - 1;
46 | }
47 | }
48 |
49 | // Total capacity of the buffer
50 | public int Capacity => _buffer.Length;
51 |
52 | public void Clear()
53 | {
54 | if (_disposed) throw new ObjectDisposedException(nameof(NetBuffer));
55 | Volatile.Write(ref _head, 0);
56 | Volatile.Write(ref _tail, 0);
57 | }
58 |
59 | public void Dispose()
60 | {
61 | if (_disposed) return;
62 | ArrayPool.Shared.Return(_buffer);
63 | _disposed = true;
64 | }
65 |
66 | // --- PRODUCER SIDE (only one thread may call these) ---
67 |
68 | public MultiMemory GetWriteSegments()
69 | {
70 | if (_disposed)
71 | return MultiMemory.Empty;
72 |
73 | int head = Volatile.Read(ref _head);
74 | int free = FreeSpace;
75 | if (free == 0)
76 | return MultiMemory.Empty;
77 |
78 | // can write up to end of array, then wrap
79 | int firstLen = Math.Min(free, _buffer.Length - head);
80 | int secondLen = free - firstLen;
81 |
82 | var first = new Memory(_buffer, head, firstLen);
83 | var second = secondLen > 0
84 | ? new Memory(_buffer, 0, secondLen)
85 | : Memory.Empty;
86 |
87 | return new MultiMemory(first, second);
88 | }
89 |
90 | ///
91 | /// Get write segments as ArraySegment for direct SAEA BufferList usage
92 | /// This avoids MemoryMarshal overhead and provides zero-alloc access
93 | ///
94 | public (ArraySegment First, ArraySegment Second) GetWriteSegmentsAsArraySegments()
95 | {
96 | if (_disposed)
97 | return (ArraySegment.Empty, ArraySegment.Empty);
98 |
99 | int head = Volatile.Read(ref _head);
100 | int free = FreeSpace;
101 | if (free == 0)
102 | return (ArraySegment.Empty, ArraySegment.Empty);
103 |
104 | // can write up to end of array, then wrap
105 | int firstLen = Math.Min(free, _buffer.Length - head);
106 | int secondLen = free - firstLen;
107 |
108 | var first = new ArraySegment(_buffer, head, firstLen);
109 | var second = secondLen > 0
110 | ? new ArraySegment(_buffer, 0, secondLen)
111 | : ArraySegment.Empty;
112 |
113 | return (first, second);
114 | }
115 |
116 | public void AdvanceWrite(int count)
117 | {
118 | if (_disposed) throw new ObjectDisposedException(nameof(NetBuffer));
119 | if (count < 0)
120 | throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be negative");
121 |
122 | int free = FreeSpace;
123 |
124 | // overwrite old data?
125 | if (count > free)
126 | {
127 | OnWarning?.Invoke(
128 | $"Buffer full: dropping entire incoming chunk of {count} bytes.");
129 | return;
130 | }
131 |
132 | // advance head
133 | int head = Volatile.Read(ref _head);
134 | int newHead = head + count;
135 | if (newHead >= _buffer.Length) newHead -= _buffer.Length;
136 | Volatile.Write(ref _head, newHead);
137 | }
138 |
139 | // --- CONSUMER SIDE (only one thread may call these) ---
140 |
141 | public MultiMemory GetReadSegments()
142 | {
143 | if (_disposed)
144 | return MultiMemory.Empty;
145 |
146 | int head = Volatile.Read(ref _head);
147 | int tail = Volatile.Read(ref _tail);
148 | int len = head >= tail
149 | ? head - tail
150 | : head + _buffer.Length - tail;
151 |
152 | if (len == 0)
153 | return MultiMemory.Empty;
154 |
155 | int firstLen = Math.Min(len, _buffer.Length - tail);
156 | int secondLen = len - firstLen;
157 |
158 | var first = new Memory(_buffer, tail, firstLen);
159 | var second = secondLen > 0
160 | ? new Memory(_buffer, 0, secondLen)
161 | : Memory.Empty;
162 |
163 | return new MultiMemory(first, second);
164 | }
165 |
166 | public void AdvanceRead(int count)
167 | {
168 | if (_disposed) throw new ObjectDisposedException(nameof(NetBuffer));
169 | if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
170 |
171 | int head = Volatile.Read(ref _head);
172 | int tail = Volatile.Read(ref _tail);
173 | int len = head >= tail
174 | ? head - tail
175 | : head + _buffer.Length - tail;
176 |
177 | if (count > len)
178 | {
179 | OnWarning?.Invoke(
180 | $"Requested to read {count}, but only {len} available. Clearing buffer.");
181 | // drop all
182 | Volatile.Write(ref _tail, head);
183 | return;
184 | }
185 |
186 | int newTail = tail + count;
187 | if (newTail >= _buffer.Length) newTail -= _buffer.Length;
188 | Volatile.Write(ref _tail, newTail);
189 | }
190 | }
--------------------------------------------------------------------------------
/Miku.PerformanceTest/README.md:
--------------------------------------------------------------------------------
1 | # Miku Performance Test Tool
2 |
3 | A unified console application for measuring the performance of the Miku networking library.
4 |
5 | This tool addresses [Issue #1](https://github.com/JasonXuDeveloper/Miku/issues/1) by providing comprehensive performance testing capabilities to measure "messages per second" for different message sizes and scenarios.
6 |
7 | ## Features
8 |
9 | - **Single unified application** with server and client commands
10 | - **Multiple test scenarios**: Echo, broadcast, silent (server) / burst, sustained, latency (client)
11 | - **Real-time performance metrics**: Messages/sec, MB/sec, round-trip times
12 | - **Configurable parameters**: Message sizes, rates, duration, client counts
13 | - **Interactive server commands**: Change modes, reset metrics, view detailed stats
14 | - **Interactive menu**: Run without any arguments to launch an interactive prompt for Server or Client modes
15 | - **Live controls**: Press `R` to reset the current metrics during a live test
16 |
17 | ## Installation & Build
18 |
19 | ```bash
20 | # Build the application
21 | dotnet build Miku.PerformanceTest/Miku.PerformanceTest.csproj
22 |
23 | # Or build the entire solution
24 | dotnet build
25 | ```
26 |
27 | ## Usage
28 |
29 | The application provides two main commands: `server` and `client`.
30 |
31 | ### Interactive Mode
32 |
33 | Running the tool without any arguments starts an interactive menu to select Server or Client mode and enter parameters.
34 |
35 | ### Server Mode
36 |
37 | Start a performance test server that can operate in different modes:
38 |
39 | ```bash
40 | # Basic server (default: 127.0.0.1:55550, echo mode)
41 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj server
42 |
43 | # Custom server configuration
44 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj server --ip 0.0.0.0 --port 8080 --buffersize 131072 --mode broadcast
45 |
46 | # See all server options
47 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj server --help
48 | ```
49 |
50 | #### Server Options:
51 | - `--ip`: IP address to bind to (default: 127.0.0.1)
52 | - `--port`: Port to listen on (default: 55550)
53 | - `--buffersize`: Buffer size per client connection in bytes (default: 65536)
54 | - `--mode`: Server mode - `echo`, `broadcast`, or `silent` (default: echo)
55 |
56 | #### Server Modes:
57 | 1. **Echo**: Server echoes received messages back to sender (good for latency testing)
58 | 2. **Broadcast**: Server broadcasts every received message to all connected clients
59 | 3. **Silent**: Server receives messages but doesn't send responses (pure receive testing)
60 |
61 | #### Live Display Controls
62 | During a running test (server or client), press:
63 | - `R` to reset the performance metrics display.
64 |
65 | ### Client Mode
66 |
67 | Run performance tests against a server:
68 |
69 | ```bash
70 | # Basic client test (100 bytes, 100 msg/s, 30 seconds, 1 client, burst mode)
71 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj client
72 |
73 | # Custom client test
74 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj client --size 300 --rate 50 --duration 60 --clients 10 --mode sustained
75 |
76 | # See all client options
77 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj client --help
78 | ```
79 |
80 | #### Client Options:
81 | - `--ip`: Server IP address (default: 127.0.0.1)
82 | - `--port`: Server port (default: 55550)
83 | - `--buffersize`: Receive buffer size in bytes (should be >= message size) (default: 1024)
84 | - `--size`: Message size in bytes (default: 100)
85 | - `--rate`: Messages per second (default: 100)
86 | - `--duration`: Test duration in seconds (default: 30)
87 | - `--clients`: Number of concurrent clients (default: 1)
88 | - `--mode`: Test mode - `burst`, `sustained`, or `latency` (default: burst)
89 |
90 | #### Client Test Modes:
91 | 1. **Burst**: Sends messages at specified rate from all clients simultaneously
92 | 2. **Sustained**: Distributes total message load evenly across clients and time
93 | 3. **Latency**: Focuses on round-trip time measurements with delays between messages
94 |
95 | ## Example Test Scenarios
96 |
97 | ### 1. Basic Echo Test (100-byte messages as requested in Issue #1)
98 |
99 | ```bash
100 | # Terminal 1: Start echo server
101 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj server
102 |
103 | # Terminal 2: Run client with 100-byte messages
104 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj client --size 100 --rate 100 --duration 30
105 | ```
106 |
107 | ### 2. Broadcast Test with Multiple Clients
108 |
109 | ```bash
110 | # Terminal 1: Start broadcast server
111 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj server --mode broadcast
112 |
113 | # Terminal 2: Run multiple clients
114 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj client --clients 5 --size 300 --rate 500
115 | ```
116 |
117 | ### 3. Latency Testing (300 and 500 bytes as mentioned in the issue)
118 |
119 | ```bash
120 | # Terminal 1: Start echo server
121 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj server
122 |
123 | # Terminal 2: Test different message sizes
124 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj client --mode latency --size 300 --duration 60
125 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj client --mode latency --size 500 --duration 60
126 | ```
127 |
128 | ### 4. High-Throughput Silent Test
129 |
130 | ```bash
131 | # Terminal 1: Start silent server (no responses)
132 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj server --mode silent
133 |
134 | # Terminal 2: Run high-rate sustained test
135 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj client --mode sustained --rate 10000 --clients 10 --size 500 --duration 120
136 | ```
137 |
138 | ## Performance Metrics
139 |
140 | ### Messages per Second (msg/s)
141 | - Primary metric requested in Issue #1
142 | - Number of individual messages processed per second
143 | - Useful for comparing performance across different message sizes
144 |
145 | ### Throughput (MB/s)
146 | - Total data throughput in megabytes per second
147 | - Better indicator of bandwidth utilization
148 | - Complements message rate for complete performance picture
149 |
150 | ### Round-Trip Time (ms)
151 | - Time from sending a message to receiving the response
152 | - Critical for interactive applications
153 | - Only meaningful in echo mode or latency testing
154 |
155 | ## Contributing
156 |
157 | When testing the library performance:
158 |
159 | 1. Test with different message sizes (100, 300, 500 bytes as requested in Issue #1)
160 | 2. Test with various client counts (1, 5, 10, 50+ clients)
161 | 3. Test different scenarios (echo, broadcast, sustained vs burst)
162 | 4. Consider hardware and network limitations when interpreting results
163 | 5. Report results with full context (hardware, network, test parameters)
164 |
165 | This addresses [Issue #1](https://github.com/JasonXuDeveloper/Miku/issues/1) by providing a comprehensive, easy-to-use performance testing tool that measures "messages per second" for different message sizes and scenarios as requested.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Miku
2 |
3 | High performance TCP server/client library
4 |
5 | ## Features
6 | - Asynchronous I/O based TCP server/client
7 | - Accepting Middlewares to handle incoming and outgoing data
8 | - Zero-allocation ring buffers for high-throughput streaming
9 | - Event-driven, non-blocking operations with back-pressure support
10 | - Built-in performance metrics: messages/sec, MB/sec, latency
11 | - Cross-platform support: .NET Standard 2.1, .NET 6.0, .NET 8.0
12 | - Extensible pipeline for framing, compression, encryption
13 |
14 | ## Performance Testing
15 |
16 | The project includes a unified performance test tool measures "messages per second" and throughput for different scenarios:
17 |
18 | - **Single unified application** with server and client commands
19 | - **Multiple test scenarios**: Echo, broadcast, silent (server) / burst, sustained, latency (client)
20 | - **Configurable parameters**: Message sizes, rates, duration, client counts
21 | - **Real-time performance metrics**: Messages/sec, MB/sec, round-trip times
22 |
23 | ### Quick Start
24 | ```bash
25 | # Run server
26 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj server
27 |
28 | # Run client (in another terminal)
29 | dotnet run --project Miku.PerformanceTest/Miku.PerformanceTest.csproj client --size 100 --rate 100 --duration 30
30 | ```
31 |
32 | See [Miku.PerformanceTest/README.md](Miku.PerformanceTest/README.md) for detailed usage instructions and performance testing scenarios.
33 |
34 | ## Documentation
35 |
36 | ### NetClient
37 |
38 | NetClient allows you to connect to a TCP server, send/receive raw byte data, and track connection lifecycle.
39 |
40 | #### Constructors
41 | - `NetClient()`: Create a new client instance.
42 |
43 | #### Connection
44 | - `void Connect(string ip, int port, int bufferSize = 1024)`: Connect to a server at the given IP and port. Optionally specify receive buffer size.
45 | - `bool IsConnected { get; }`: Indicates if the client is connected.
46 | - `string Ip { get; }`: Remote endpoint IP.
47 |
48 | #### Data Transfer
49 | - `void Send(ReadOnlyMemory data)`: Sends raw data to the server.
50 | - `event Action> OnDataReceived`: Raised when data is received from the server.
51 |
52 | #### Events & Lifecycle
53 | - `event Action OnConnected`: Raised upon successful connection.
54 | - `event Action OnDisconnected`: Raised when the client disconnects, with reason.
55 | - `event Action OnError`: Raised on errors.
56 | - `void Stop(string reason = "Connection closed by client", bool includeCallStack = false)`: Gracefully stops the client.
57 |
58 | #### Properties
59 | - `int Id { get; }`: Unique identifier for this client instance.
60 | - `int BufferSize { get; }`: Configured receive buffer capacity.
61 | - `NetClientStats Stats { get; }`: Performance statistics (messages sent/received, bytes sent/received, drops).
62 | - `bool HasPendingSends { get; }`: Whether there are pending outgoing messages.
63 | - `int PendingSendCount { get; }`: Number of messages queued for sending.
64 |
65 | #### Utilities
66 | - `void ResetStats()`: Reset all performance counters to zero.
67 | - `string GetDiagnosticInfo()`: Retrieve detailed diagnostic information (buffer usage, stats, connection state).
68 |
69 | ### NetServer
70 |
71 | NetServer listens for incoming TCP connections and dispatches data to subscribed clients.
72 |
73 | #### Configuration
74 | - `int BufferSize { get; set; }`: Size of send/receive buffer (default: 64 KiB).
75 |
76 | #### Lifecycle
77 | - `void Start(string ip, int port, int backLog = 1000)`: Begin listening on the specified IP and port.
78 | - `void Stop()`: Stop listening and disconnect all clients.
79 | - `bool IsListening { get; }`: Indicates if server is active.
80 | - `int ClientCount { get; }`: Current number of connected clients.
81 |
82 | #### Events
83 | - `event Action OnClientConnected`: Triggered when a client connects.
84 | - `event Action OnClientDisconnected`: Triggered when a client disconnects, with reason.
85 | - `event Action> OnClientDataReceived`: Triggered when a client sends data.
86 | - `event Action OnError`: Triggered on server errors.
87 |
88 | ### Middleware
89 |
90 | Implement `INetMiddleware` to process data before sending or after receiving:
91 |
92 | ```csharp
93 | public interface INetMiddleware
94 | {
95 | void ProcessSend(ReadOnlyMemory src, ArrayBufferWriter dst);
96 | (bool halt, int consumedFromOrigin) ProcessReceive(ReadOnlyMemory src, ArrayBufferWriter dst);
97 | }
98 | ```
99 |
100 | - `ProcessSend`: Transform or wrap outgoing data. Write to `dst`.
101 | - `ProcessReceive`: Transform or unwrap incoming data. Return `(halt: true, consumed)` to buffer until ready. You should only `halt` when fails to process incoming data. For `consumed`, you should return a number of consumed bytes **related** to the original buffer (e.g. what you originally received from the socket).
102 |
103 | **Built-in middleware examples:**
104 | - `PacketFrameMiddleware`: Prepends a 4-byte length header for framing.
105 | - `Lz4CompressionMiddleware`: Compresses/decompresses data with LZ4.
106 |
107 | ### Examples
108 |
109 | #### Echo Server and Client
110 |
111 | ```csharp
112 | // Server
113 | var server = new NetServer();
114 | server.OnClientConnected += c => Console.WriteLine($"Client connected: {c.Ip}");
115 | server.OnClientDataReceived += (c, data) => c.Send(data.ToArray());
116 | server.OnError += e => Console.WriteLine(e);
117 | server.Start("0.0.0.0", 54323);
118 |
119 | // Client
120 | var client = new NetClient();
121 | client.OnDataReceived += data =>
122 | {
123 | Console.WriteLine($"Echoed: {string.Join(',', data.ToArray())}");
124 | client.Stop();
125 | server.Stop();
126 | };
127 | client.Connect("127.0.0.1", 54323);
128 | client.Send(new byte[]{1,2,3,4,5});
129 | ```
130 |
131 | #### Framing Middleware
132 |
133 | ```csharp
134 | // Server
135 | var server = new NetServer();
136 | server.OnClientConnected += c => c.AddMiddleware(new PacketFrameMiddleware());
137 | server.OnClientDataReceived += (c, data) => c.Send(data.ToArray());
138 | server.Start("0.0.0.0", 54324);
139 |
140 | // Client
141 | var client = new NetClient();
142 | client.AddMiddleware(new PacketFrameMiddleware());
143 | client.OnDataReceived += data => Console.WriteLine($"Received: {string.Join(',', data.ToArray())}");
144 | client.Connect("127.0.0.1", 54324);
145 | client.Send(new byte[]{1,2,3,4,5});
146 | ```
147 |
148 | #### Compression + Framing Middleware
149 |
150 | ```csharp
151 | var server = new NetServer();
152 | server.OnClientConnected += c =>
153 | {
154 | c.AddMiddleware(new Lz4CompressionMiddleware());
155 | c.AddMiddleware(new PacketFrameMiddleware());
156 | };
157 | server.OnClientDataReceived += (c, data) => c.Send(data.ToArray());
158 | server.Start("0.0.0.0", 54324);
159 |
160 | var client = new NetClient();
161 | client.AddMiddleware(new Lz4CompressionMiddleware());
162 | client.AddMiddleware(new PacketFrameMiddleware());
163 | client.OnDataReceived += data => Console.WriteLine($"Received: {string.Join(',', data.ToArray())}");
164 | client.Connect("127.0.0.1", 54324);
165 | client.Send(new byte[]{1,2,3,4,5});
166 | ```
167 |
168 | #### Ping-Pong Protocol
169 |
170 | ```csharp
171 | // Server: deserialize Ping, reply with Pong
172 | server.OnClientConnected += c =>
173 | {
174 | c.AddMiddleware(new Lz4CompressionMiddleware());
175 | c.AddMiddleware(new PacketFrameMiddleware());
176 | };
177 | server.OnClientDataReceived += (c, data) =>
178 | {
179 | Deserializer.Deserialize(data.Span, out IProtocol msg);
180 | if (msg is Ping ping)
181 | {
182 | // validate ping
183 | c.Send(pong.Serialize());
184 | }
185 | };
186 |
187 | // Client
188 | var client = new NetClient();
189 | client.AddMiddleware(new Lz4CompressionMiddleware());
190 | client.AddMiddleware(new PacketFrameMiddleware());
191 | client.OnDataReceived += data =>
192 | {
193 | Deserializer.Deserialize(data.Span, out IProtocol msg);
194 | if (msg is Pong) Console.WriteLine("Received Pong!");
195 | client.Stop();
196 | server.Stop();
197 | };
198 | client.Connect("127.0.0.1", 54324);
199 | client.Send(ping.Serialize());
200 | ```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 | .idea/*
4 |
5 | # globs
6 | Makefile.in
7 | *.userprefs
8 | *.usertasks
9 | config.make
10 | config.status
11 | aclocal.m4
12 | install-sh
13 | autom4te.cache/
14 | *.tar.gz
15 | tarballs/
16 | test-results/
17 |
18 | # Mac bundle stuff
19 | *.dmg
20 | *.app
21 |
22 | # content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
23 | # General
24 | .DS_Store
25 | .AppleDouble
26 | .LSOverride
27 |
28 | # Icon must end with two \r
29 | Icon
30 |
31 |
32 | # Thumbnails
33 | ._*
34 |
35 | # Files that might appear in the root of a volume
36 | .DocumentRevisions-V100
37 | .fseventsd
38 | .Spotlight-V100
39 | .TemporaryItems
40 | .Trashes
41 | .VolumeIcon.icns
42 | .com.apple.timemachine.donotpresent
43 |
44 | # Directories potentially created on remote AFP share
45 | .AppleDB
46 | .AppleDesktop
47 | Network Trash Folder
48 | Temporary Items
49 | .apdisk
50 |
51 | # content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
52 | # Windows thumbnail cache files
53 | Thumbs.db
54 | ehthumbs.db
55 | ehthumbs_vista.db
56 |
57 | # Dump file
58 | *.stackdump
59 |
60 | # Folder config file
61 | [Dd]esktop.ini
62 |
63 | # Recycle Bin used on file shares
64 | $RECYCLE.BIN/
65 |
66 | # Windows Installer files
67 | *.cab
68 | *.msi
69 | *.msix
70 | *.msm
71 | *.msp
72 |
73 | # Windows shortcuts
74 | *.lnk
75 |
76 | # content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
77 | ## Ignore Visual Studio temporary files, build results, and
78 | ## files generated by popular Visual Studio add-ons.
79 | ##
80 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
81 |
82 | # User-specific files
83 | *.suo
84 | *.user
85 | *.userosscache
86 | *.sln.docstates
87 |
88 | # User-specific files (MonoDevelop/Xamarin Studio)
89 | *.userprefs
90 |
91 | # Build results
92 | [Dd]ebug/
93 | [Dd]ebugPublic/
94 | [Rr]elease/
95 | [Rr]eleases/
96 | x64/
97 | x86/
98 | bld/
99 | [Bb]in/
100 | [Oo]bj/
101 | [Ll]og/
102 |
103 | # Visual Studio 2015/2017 cache/options directory
104 | .vs/
105 | # Uncomment if you have tasks that create the project's static files in wwwroot
106 | #wwwroot/
107 |
108 | # Visual Studio 2017 auto generated files
109 | Generated\ Files/
110 |
111 | # MSTest test Results
112 | [Tt]est[Rr]esult*/
113 | [Bb]uild[Ll]og.*
114 |
115 | # NUNIT
116 | *.VisualState.xml
117 | TestResult.xml
118 |
119 | # Build Results of an ATL Project
120 | [Dd]ebugPS/
121 | [Rr]eleasePS/
122 | dlldata.c
123 |
124 | # Benchmark Results
125 | BenchmarkDotNet.Artifacts/
126 |
127 | # .NET Core
128 | project.lock.json
129 | project.fragment.lock.json
130 | artifacts/
131 |
132 | # StyleCop
133 | StyleCopReport.xml
134 |
135 | # Files built by Visual Studio
136 | *_i.c
137 | *_p.c
138 | *_h.h
139 | *.ilk
140 | *.meta
141 | *.obj
142 | *.iobj
143 | *.pch
144 | *.pdb
145 | *.ipdb
146 | *.pgc
147 | *.pgd
148 | *.rsp
149 | *.sbr
150 | *.tlb
151 | *.tli
152 | *.tlh
153 | *.tmp
154 | *.tmp_proj
155 | *_wpftmp.csproj
156 | *.log
157 | *.vspscc
158 | *.vssscc
159 | .builds
160 | *.pidb
161 | *.svclog
162 | *.scc
163 |
164 | # Chutzpah Test files
165 | _Chutzpah*
166 |
167 | # Visual C++ cache files
168 | ipch/
169 | *.aps
170 | *.ncb
171 | *.opendb
172 | *.opensdf
173 | *.sdf
174 | *.cachefile
175 | *.VC.db
176 | *.VC.VC.opendb
177 |
178 | # Visual Studio profiler
179 | *.psess
180 | *.vsp
181 | *.vspx
182 | *.sap
183 |
184 | # Visual Studio Trace Files
185 | *.e2e
186 |
187 | # TFS 2012 Local Workspace
188 | $tf/
189 |
190 | # Guidance Automation Toolkit
191 | *.gpState
192 |
193 | # ReSharper is a .NET coding add-in
194 | _ReSharper*/
195 | *.[Rr]e[Ss]harper
196 | *.DotSettings.user
197 |
198 | # JustCode is a .NET coding add-in
199 | .JustCode
200 |
201 | # TeamCity is a build add-in
202 | _TeamCity*
203 |
204 | # DotCover is a Code Coverage Tool
205 | *.dotCover
206 |
207 | # AxoCover is a Code Coverage Tool
208 | .axoCover/*
209 | !.axoCover/settings.json
210 |
211 | # Visual Studio code coverage results
212 | *.coverage
213 | *.coveragexml
214 |
215 | # NCrunch
216 | _NCrunch_*
217 | .*crunch*.local.xml
218 | nCrunchTemp_*
219 |
220 | # MightyMoose
221 | *.mm.*
222 | AutoTest.Net/
223 |
224 | # Web workbench (sass)
225 | .sass-cache/
226 |
227 | # Installshield output folder
228 | [Ee]xpress/
229 |
230 | # DocProject is a documentation generator add-in
231 | DocProject/buildhelp/
232 | DocProject/Help/*.HxT
233 | DocProject/Help/*.HxC
234 | DocProject/Help/*.hhc
235 | DocProject/Help/*.hhk
236 | DocProject/Help/*.hhp
237 | DocProject/Help/Html2
238 | DocProject/Help/html
239 |
240 | # Click-Once directory
241 | publish/
242 |
243 | # Publish Web Output
244 | *.[Pp]ublish.xml
245 | *.azurePubxml
246 | # Note: Comment the next line if you want to checkin your web deploy settings,
247 | # but database connection strings (with potential passwords) will be unencrypted
248 | *.pubxml
249 | *.publishproj
250 |
251 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
252 | # checkin your Azure Web App publish settings, but sensitive information contained
253 | # in these scripts will be unencrypted
254 | PublishScripts/
255 |
256 | # NuGet Packages
257 | *.nupkg
258 | # The packages folder can be ignored because of Package Restore
259 | **/[Pp]ackages/*
260 | # except build/, which is used as an MSBuild target.
261 | !**/[Pp]ackages/build/
262 | # Uncomment if necessary however generally it will be regenerated when needed
263 | #!**/[Pp]ackages/repositories.config
264 | # NuGet v3's project.json files produces more ignorable files
265 | *.nuget.props
266 | *.nuget.targets
267 |
268 | # Microsoft Azure Build Output
269 | csx/
270 | *.build.csdef
271 |
272 | # Microsoft Azure Emulator
273 | ecf/
274 | rcf/
275 |
276 | # Windows Store app package directories and files
277 | AppPackages/
278 | BundleArtifacts/
279 | Package.StoreAssociation.xml
280 | _pkginfo.txt
281 | *.appx
282 |
283 | # Visual Studio cache files
284 | # files ending in .cache can be ignored
285 | *.[Cc]ache
286 | # but keep track of directories ending in .cache
287 | !*.[Cc]ache/
288 |
289 | # Others
290 | ClientBin/
291 | ~$*
292 | *~
293 | *.dbmdl
294 | *.dbproj.schemaview
295 | *.jfm
296 | *.pfx
297 | *.publishsettings
298 | orleans.codegen.cs
299 |
300 | # Including strong name files can present a security risk
301 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
302 | #*.snk
303 |
304 | # Since there are multiple workflows, uncomment next line to ignore bower_components
305 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
306 | #bower_components/
307 |
308 | # RIA/Silverlight projects
309 | Generated_Code/
310 |
311 | # Backup & report files from converting an old project file
312 | # to a newer Visual Studio version. Backup files are not needed,
313 | # because we have git ;-)
314 | _UpgradeReport_Files/
315 | Backup*/
316 | UpgradeLog*.XML
317 | UpgradeLog*.htm
318 | ServiceFabricBackup/
319 | *.rptproj.bak
320 |
321 | # SQL Server files
322 | *.mdf
323 | *.ldf
324 | *.ndf
325 |
326 | # Business Intelligence projects
327 | *.rdl.data
328 | *.bim.layout
329 | *.bim_*.settings
330 | *.rptproj.rsuser
331 |
332 | # Microsoft Fakes
333 | FakesAssemblies/
334 |
335 | # GhostDoc plugin setting file
336 | *.GhostDoc.xml
337 |
338 | # Node.js Tools for Visual Studio
339 | .ntvs_analysis.dat
340 | node_modules/
341 |
342 | # Visual Studio 6 build log
343 | *.plg
344 |
345 | # Visual Studio 6 workspace options file
346 | *.opt
347 |
348 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
349 | *.vbw
350 |
351 | # Visual Studio LightSwitch build output
352 | **/*.HTMLClient/GeneratedArtifacts
353 | **/*.DesktopClient/GeneratedArtifacts
354 | **/*.DesktopClient/ModelManifest.xml
355 | **/*.Server/GeneratedArtifacts
356 | **/*.Server/ModelManifest.xml
357 | _Pvt_Extensions
358 |
359 | # Paket dependency manager
360 | .paket/paket.exe
361 | paket-files/
362 |
363 | # FAKE - F# Make
364 | .fake/
365 |
366 | # JetBrains Rider
367 | .idea/
368 | *.sln.iml
369 |
370 | # CodeRush personal settings
371 | .cr/personal
372 |
373 | # Python Tools for Visual Studio (PTVS)
374 | __pycache__/
375 | *.pyc
376 |
377 | # Cake - Uncomment if you are using it
378 | # tools/**
379 | # !tools/packages.config
380 |
381 | # Tabs Studio
382 | *.tss
383 |
384 | # Telerik's JustMock configuration file
385 | *.jmconfig
386 |
387 | # BizTalk build output
388 | *.btp.cs
389 | *.btm.cs
390 | *.odx.cs
391 | *.xsd.cs
392 |
393 | # OpenCover UI analysis results
394 | OpenCover/
395 |
396 | # Azure Stream Analytics local run output
397 | ASALocalRun/
398 |
399 | # MSBuild Binary and Structured Log
400 | *.binlog
401 |
402 | # NVidia Nsight GPU debugger configuration file
403 | *.nvuser
404 |
405 | # MFractors (Xamarin productivity tool) working folder
406 | .mfractor/
407 |
408 | # Local History for Visual Studio
409 | .localhistory/
410 |
--------------------------------------------------------------------------------
/Miku.Core/NetServer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net;
4 | using System.Net.Sockets;
5 | using System.Threading;
6 |
7 | namespace Miku.Core
8 | {
9 | ///
10 | /// A server that listens for incoming connections from clients.
11 | ///
12 | public class NetServer : IDisposable
13 | {
14 | ///
15 | /// Event that is raised when a new client connects to the server.
16 | ///
17 | public event Action OnClientConnected;
18 |
19 | ///
20 | /// Event that is raised when a client disconnects from the server.
21 | ///
22 | public event Action OnClientDisconnected;
23 |
24 | ///
25 | /// Event that is raised when a client sends data to the server.
26 | ///
27 | public event Action> OnClientDataReceived;
28 |
29 | ///
30 | /// Event that is raised when an error occurs.
31 | ///
32 | public event Action OnError;
33 |
34 | ///
35 | /// The size of the buffer used for sending and receiving data.
36 | ///
37 | public int BufferSize { get; set; } = 64 * 1024;
38 |
39 | private Socket _listenSocket;
40 | private SocketAsyncEventArgs _acceptEventArgs;
41 | private readonly List _clients = new();
42 | private readonly ReaderWriterLockSlim _clientsLock = new();
43 | private volatile bool _isListening;
44 | private int _disposed = 0;
45 |
46 | ///
47 | /// Whether the server is currently listening for connections
48 | ///
49 | public bool IsListening => _isListening && _disposed == 0;
50 |
51 | ///
52 | /// Get current client count in a thread-safe manner
53 | ///
54 | public int ClientCount
55 | {
56 | get
57 | {
58 | _clientsLock.EnterReadLock();
59 | try
60 | {
61 | return _clients.Count;
62 | }
63 | finally
64 | {
65 | _clientsLock.ExitReadLock();
66 | }
67 | }
68 | }
69 |
70 | ///
71 | /// Starts the server and begins listening for new connections.
72 | ///
73 | ///
74 | ///
75 | ///
76 | public void Start(string ip, int port, int backLog = 1000)
77 | {
78 | if (_disposed != 0)
79 | {
80 | throw new ObjectDisposedException(nameof(NetServer));
81 | }
82 |
83 | if (_isListening)
84 | {
85 | throw new InvalidOperationException("Server is already listening");
86 | }
87 |
88 | if (string.IsNullOrWhiteSpace(ip))
89 | {
90 | throw new ArgumentException("IP address cannot be null or empty", nameof(ip));
91 | }
92 |
93 | if (port <= 0 || port > 65535)
94 | {
95 | throw new ArgumentException("Port must be between 1 and 65535", nameof(port));
96 | }
97 |
98 | try
99 | {
100 | _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
101 |
102 | // Optimize server socket settings
103 | _listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
104 | _listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, false);
105 |
106 | _listenSocket.Bind(new IPEndPoint(IPAddress.Parse(ip), port));
107 | _listenSocket.Listen(backLog);
108 |
109 | // Set up the SocketAsyncEventArgs for accepting connections.
110 | _acceptEventArgs = new SocketAsyncEventArgs();
111 | _acceptEventArgs.Completed += ProcessAccept;
112 |
113 | _isListening = true;
114 |
115 | // Start the first accept operation.
116 | StartAccept();
117 | }
118 | catch (Exception ex)
119 | {
120 | // Clean up on failure
121 | _isListening = false;
122 | _listenSocket?.Close();
123 | _listenSocket?.Dispose();
124 | _listenSocket = null;
125 | _acceptEventArgs?.Dispose();
126 | _acceptEventArgs = null;
127 |
128 | OnError?.Invoke(ex);
129 | throw;
130 | }
131 | }
132 |
133 | ///
134 | /// Stops the server from listening for new connections.
135 | ///
136 | public void Stop()
137 | {
138 | if (!_isListening) return;
139 |
140 | _isListening = false;
141 |
142 | try
143 | {
144 | _listenSocket?.Close();
145 | }
146 | catch (Exception ex)
147 | {
148 | OnError?.Invoke(ex);
149 | }
150 | finally
151 | {
152 | _listenSocket?.Dispose();
153 | _listenSocket = null;
154 | }
155 |
156 | _clientsLock.EnterWriteLock();
157 | try
158 | {
159 | // Stop all clients, make a copy of the list to avoid modifying it while iterating.
160 | foreach (var client in _clients.ToArray())
161 | {
162 | try
163 | {
164 | client.Stop("Server shutting down");
165 | }
166 | catch (Exception ex)
167 | {
168 | OnError?.Invoke(ex);
169 | }
170 | }
171 |
172 | _clients.Clear();
173 | }
174 | finally
175 | {
176 | _clientsLock.ExitWriteLock();
177 | }
178 |
179 | _acceptEventArgs?.Dispose();
180 | _acceptEventArgs = null;
181 | }
182 |
183 | private void StartAccept()
184 | {
185 | if (!_isListening) return;
186 |
187 | _acceptEventArgs.AcceptSocket = null;
188 |
189 | if (!_listenSocket.AcceptAsync(_acceptEventArgs))
190 | {
191 | ProcessAccept(null, _acceptEventArgs);
192 | }
193 | }
194 |
195 | private void ProcessAccept(object sender, SocketAsyncEventArgs e)
196 | {
197 | if (e.SocketError == SocketError.Success)
198 | {
199 | // Retrieve the accepted socket.
200 | Socket acceptedSocket = e.AcceptSocket;
201 |
202 | // Create a new NetClient using the accepted socket.
203 | #pragma warning disable IDISP001
204 | NetClient client = new NetClient();
205 | #pragma warning restore IDISP001
206 |
207 | _clientsLock.EnterWriteLock();
208 | try
209 | {
210 | _clients.Add(client);
211 | }
212 | finally
213 | {
214 | _clientsLock.ExitWriteLock();
215 | }
216 |
217 | client.OnConnected += () => OnClientConnected?.Invoke(client);
218 | client.OnDisconnected += reason =>
219 | {
220 | OnClientDisconnected?.Invoke(client, reason);
221 | _clientsLock.EnterWriteLock();
222 | try
223 | {
224 | _clients.Remove(client);
225 | }
226 | finally
227 | {
228 | _clientsLock.ExitWriteLock();
229 | }
230 | };
231 | client.OnDataReceived += data => OnClientDataReceived?.Invoke(client, data);
232 | client.OnError += OnError;
233 | client.Connect(acceptedSocket, BufferSize);
234 | }
235 | // If the accept operation was canceled, then the server is stopped.
236 | else if (e.SocketError != SocketError.Interrupted && e.SocketError != SocketError.OperationAborted)
237 | {
238 | OnError?.Invoke(new SocketException((int)e.SocketError));
239 | }
240 |
241 | // Continue accepting the next connection only if still listening
242 | if (_isListening && _listenSocket != null)
243 | {
244 | StartAccept();
245 | }
246 | }
247 |
248 | public void Dispose()
249 | {
250 | if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0)
251 | {
252 | Stop();
253 | _clientsLock?.Dispose();
254 | }
255 | }
256 | }
257 | }
--------------------------------------------------------------------------------
/Miku.UnitTest/ServerTests.cs:
--------------------------------------------------------------------------------
1 | using Miku.Core;
2 | using Miku.UnitTest.NinoGen;
3 | using Miku.UnitTest.Protocol;
4 |
5 | namespace Miku.UnitTest;
6 |
7 | public class ServerTests
8 | {
9 | [SetUp]
10 | public void Setup()
11 | {
12 | TaskScheduler.UnobservedTaskException += (_, e) =>
13 | {
14 | Console.WriteLine(e.Exception);
15 | e.SetObserved();
16 | };
17 |
18 | AppDomain.CurrentDomain.UnhandledException += (_, e) => { Console.WriteLine(e.ExceptionObject); };
19 | }
20 |
21 | [Test, CancelAfter(1000)]
22 | public async Task ServerReceiveTest()
23 | {
24 | // A data for testing.
25 | var buffer = new byte[] { 1, 2, 3, 4, 5 };
26 | // ip port info
27 | var ip = "0.0.0.0";
28 | var port = 54321;
29 | // await for assertion
30 | TaskCompletionSource tcs = new TaskCompletionSource();
31 |
32 | // Create a new server.
33 | var server = new NetServer();
34 | server.OnClientConnected += client => { Console.WriteLine($"Client connected: {client.Ip}"); };
35 | server.OnClientDisconnected += (client, reason) =>
36 | {
37 | Console.WriteLine($"Client disconnected: {client.Ip}, reason: {reason}");
38 | server.Stop();
39 | };
40 | server.OnClientDataReceived += (client, data) =>
41 | {
42 | Console.WriteLine($"Data received from {client.Ip}: {string.Join(',', data.ToArray())}");
43 | // Assert the data received from the client is sequentially equal to the buffer.
44 | tcs.SetResult(data.ToArray().SequenceEqual(buffer));
45 | };
46 | server.OnError += exception =>
47 | {
48 | Console.WriteLine($"An error occurred: {exception} {exception.StackTrace}");
49 | throw exception;
50 | };
51 |
52 | server.Start(ip, port);
53 |
54 | // Simulate a client connecting to the server.
55 | var client = new NetClient();
56 | client.Connect(ip, port);
57 |
58 | // Simulate the client sending data to the server.
59 | client.Send(buffer);
60 |
61 | // Close the client.
62 | client.Stop();
63 |
64 | // await for the server to stop
65 | Assert.That(await tcs.Task);
66 |
67 | // Stop the server.
68 | server.Stop();
69 | }
70 |
71 | [Test, CancelAfter(1000)]
72 | public async Task ServerStopsClientTest()
73 | {
74 | // A data for testing.
75 | var buffer = new byte[] { 1, 2, 3, 4, 5 };
76 | // ip port info
77 | var ip = "0.0.0.0";
78 | var port = 54322;
79 | // await for assertion
80 | TaskCompletionSource tcs = new TaskCompletionSource();
81 |
82 | // Create a new server.
83 | var server = new NetServer();
84 | server.OnClientConnected += client => { Console.WriteLine($"Client connected: {client.Ip}"); };
85 | server.OnClientDisconnected += (client, reason) =>
86 | {
87 | Console.WriteLine($"Client disconnected: {client.Ip}, reason: {reason}");
88 | };
89 | server.OnClientDataReceived += (client, data) =>
90 | {
91 | Console.WriteLine($"Data received from {client.Ip}: {string.Join(',', data.ToArray())}");
92 | server.Stop();
93 | };
94 | server.OnError += exception =>
95 | {
96 | Console.WriteLine($"An error occurred: {exception} {exception.StackTrace}");
97 | throw exception;
98 | };
99 |
100 | server.Start(ip, port);
101 |
102 | // Simulate a client connecting to the server.
103 | var client = new NetClient();
104 | client.OnDisconnected += (reason) =>
105 | {
106 | Console.WriteLine($"Client disconnected: {reason}");
107 | tcs.SetResult(true);
108 | };
109 |
110 | client.Connect(ip, port);
111 |
112 | // Simulate the client sending data to the server.
113 | client.Send(buffer);
114 |
115 | // await for the server to stop
116 | Assert.That(await tcs.Task);
117 |
118 | // Stop the server.
119 | server.Stop();
120 | }
121 |
122 | [Test, CancelAfter(1000)]
123 | public async Task EchoTest()
124 | {
125 | // A data for testing.
126 | var buffer = new byte[] { 1, 2, 3, 4, 5 };
127 | // ip port info
128 | var ip = "0.0.0.0";
129 | var port = 54323;
130 | // await for assertion
131 | TaskCompletionSource tcs = new TaskCompletionSource();
132 |
133 | // Create a new server.
134 | var server = new NetServer();
135 | server.OnClientConnected += client => { Console.WriteLine($"Client connected: {client.Ip}"); };
136 | server.OnClientDisconnected += (client, reason) => { Console.WriteLine($"Client disconnected: {client.Ip}, reason: {reason}"); };
137 | server.OnClientDataReceived += (client, data) =>
138 | {
139 | Console.WriteLine($"Data received from {client.Ip}: {string.Join(',', data.ToArray())}");
140 | // Send the data back to the client.
141 | client.Send(data.ToArray());
142 | };
143 | server.OnError += exception =>
144 | {
145 | Console.WriteLine($"An error occurred: {exception} {exception.StackTrace}");
146 | throw exception;
147 | };
148 |
149 | server.Start(ip, port);
150 |
151 | // Simulate a client connecting to the server.
152 | var client = new NetClient();
153 | client.OnDataReceived += (data) =>
154 | {
155 | Console.WriteLine($"Data received from server: {string.Join(',', data.ToArray())}");
156 | client.Stop();
157 | tcs.SetResult(data.ToArray().SequenceEqual(buffer));
158 | };
159 |
160 | client.Connect(ip, port);
161 |
162 | // Simulate the client sending data to the server.
163 | client.Send(buffer);
164 |
165 | // await for the server to stop
166 | Assert.That(await tcs.Task);
167 |
168 | // Stop the server.
169 | server.Stop();
170 | }
171 |
172 | [Test, CancelAfter(1000)]
173 | public async Task FramingMiddlewareTest()
174 | {
175 | // A data for testing.
176 | var buffer = new byte[] { 1, 2, 3, 4, 5 };
177 | // ip port info
178 | var ip = "0.0.0.0";
179 | var port = 54324;
180 | // await for assertion
181 | TaskCompletionSource tcs = new TaskCompletionSource();
182 |
183 | // Create a new server.
184 | var server = new NetServer();
185 | server.OnClientConnected += client =>
186 | {
187 | Console.WriteLine($"Client connected: {client.Ip}");
188 | // Add the middleware to the client.
189 | client.AddMiddleware(new PacketFrameMiddleware());
190 | };
191 | server.OnClientDisconnected += (client, reason) => { Console.WriteLine($"Client disconnected: {client.Ip}, reason: {reason}"); };
192 | server.OnClientDataReceived += (client, data) =>
193 | {
194 | Console.WriteLine($"Data received from {client.Ip}: {string.Join(',', data.ToArray())}");
195 | // Send the data back to the client.
196 | client.Send(data.ToArray());
197 | };
198 | server.OnError += exception =>
199 | {
200 | Console.WriteLine($"An error occurred: {exception} {exception.StackTrace}");
201 | throw exception;
202 | };
203 |
204 | server.Start(ip, port);
205 |
206 | // Simulate a client connecting to the server.
207 | var client = new NetClient();
208 | client.OnDataReceived += (data) =>
209 | {
210 | Console.WriteLine($"Data received from server: {string.Join(',', data.ToArray())}");
211 | client.Stop();
212 | tcs.SetResult(data.ToArray().SequenceEqual(buffer));
213 | };
214 | // Add the middleware to the client.
215 | client.AddMiddleware(new PacketFrameMiddleware());
216 | client.Connect(ip, port);
217 |
218 | // Simulate the client sending data to the server.
219 | client.Send(buffer);
220 |
221 | // await for the server to stop
222 | Assert.That(await tcs.Task);
223 |
224 | // Stop the server.
225 | server.Stop();
226 | }
227 |
228 | [Test, CancelAfter(1000)]
229 | public async Task MultipleMiddlewareTest()
230 | {
231 | // A data for testing.
232 | var buffer = new byte[] { 1, 2, 3, 4, 5 };
233 | // ip port info
234 | var ip = "0.0.0.0";
235 | var port = 54324;
236 | // await for assertion
237 | TaskCompletionSource tcs = new TaskCompletionSource();
238 |
239 | // Create a new server.
240 | var server = new NetServer();
241 | server.OnClientConnected += client =>
242 | {
243 | Console.WriteLine($"Client connected: {client.Ip}");
244 | // Add the middleware to the client. When send, lz4 first, then frame
245 | client.AddMiddleware(new Lz4CompressionMiddleware());
246 | client.AddMiddleware(new PacketFrameMiddleware());
247 | };
248 | server.OnClientDisconnected += (client, reason) => { Console.WriteLine($"Client disconnected: {client.Ip}, reason: {reason}"); };
249 | server.OnClientDataReceived += (client, data) =>
250 | {
251 | Console.WriteLine($"Data received from {client.Ip}: {string.Join(',', data.ToArray())}");
252 | // Send the data back to the client.
253 | client.Send(data.ToArray());
254 | };
255 | server.OnError += exception =>
256 | {
257 | Console.WriteLine($"An error occurred: {exception} {exception.StackTrace}");
258 | throw exception;
259 | };
260 |
261 | server.Start(ip, port);
262 |
263 | // Simulate a client connecting to the server.
264 | var client = new NetClient();
265 | client.OnDataReceived += (data) =>
266 | {
267 | Console.WriteLine($"Data received from server: {string.Join(',', data.ToArray())}");
268 | client.Stop();
269 | tcs.SetResult(data.ToArray().SequenceEqual(buffer));
270 | };
271 | // Add the middleware to the client.
272 | client.AddMiddleware(new Lz4CompressionMiddleware());
273 | client.AddMiddleware(new PacketFrameMiddleware());
274 | client.Connect(ip, port);
275 |
276 | // Simulate the client sending data to the server.
277 | client.Send(buffer);
278 |
279 | // await for the server to stop
280 | Assert.That(await tcs.Task);
281 |
282 | // Stop the server.
283 | server.Stop();
284 | }
285 |
286 | [Test, CancelAfter(1000)]
287 | public async Task PingPongTest()
288 | {
289 | // ip port info
290 | var ip = "0.0.0.0";
291 | var port = 54324;
292 | // await for assertion
293 | TaskCompletionSource tcs = new TaskCompletionSource();
294 | Ping dataToSend = new Ping
295 | {
296 | Field1 = 1,
297 | Field2 = "2",
298 | Field3 = 3.5f,
299 | Field4 = 4.5,
300 | Field5 = 5,
301 | Field6 = 6,
302 | Field7 = 7,
303 | Field8 = Guid.NewGuid()
304 | };
305 | Pong dataToReceive = new Pong
306 | {
307 | Field1 = 1,
308 | Field2 = "2",
309 | Field3 = 3.5f,
310 | Field4 = 4.5,
311 | Field5 = 5,
312 | Field6 = 6,
313 | Field7 = 7,
314 | Field8 = Guid.NewGuid()
315 | };
316 |
317 | // Create a new server.
318 | var server = new NetServer();
319 | server.OnClientConnected += client =>
320 | {
321 | Console.WriteLine($"Client connected: {client.Ip}");
322 | // Add the middleware to the client. When send, lz4 first, then frame
323 | client.AddMiddleware(new Lz4CompressionMiddleware());
324 | client.AddMiddleware(new PacketFrameMiddleware());
325 | };
326 | server.OnClientDisconnected += (client, reason) => { Console.WriteLine($"Client disconnected: {client.Ip}, reason: {reason}"); };
327 | server.OnClientDataReceived += (client, data) =>
328 | {
329 | Console.WriteLine($"Data received from {client.Ip}: {string.Join(',', data.ToArray())}");
330 | // Send the data back to the client.
331 | Deserializer.Deserialize(data.Span, out IProtocol value);
332 | switch (value)
333 | {
334 | case Ping ping:
335 | if (!ping.Equals(dataToSend))
336 | throw new Exception("Ping data is not equal to the data sent");
337 | client.Send(dataToReceive.Serialize());
338 | break;
339 | default:
340 | throw new ArgumentOutOfRangeException();
341 | }
342 | };
343 | server.OnError += exception =>
344 | {
345 | Console.WriteLine($"An error occurred: {exception} {exception.StackTrace}");
346 | throw exception;
347 | };
348 |
349 | server.Start(ip, port);
350 |
351 | // Simulate a client connecting to the server.
352 | var client = new NetClient();
353 | client.OnDataReceived += (data) =>
354 | {
355 | Console.WriteLine($"Data received from server: {string.Join(',', data.ToArray())}");
356 | client.Stop();
357 | Deserializer.Deserialize(data.Span, out IProtocol value);
358 | tcs.SetResult(value);
359 | };
360 | // Add the middleware to the client.
361 | client.AddMiddleware(new Lz4CompressionMiddleware());
362 | client.AddMiddleware(new PacketFrameMiddleware());
363 | client.Connect(ip, port);
364 |
365 | // Simulate the client sending data to the server.
366 | client.Send(dataToSend.Serialize());
367 |
368 | // await for the server to stop
369 | Assert.That(await tcs.Task is Pong pong && pong.Equals(dataToReceive));
370 |
371 | // Stop the server.
372 | server.Stop();
373 | }
374 | }
--------------------------------------------------------------------------------
/Miku.PerformanceTest/Program.cs:
--------------------------------------------------------------------------------
1 | using ConsoleAppFramework;
2 | using Miku.Core;
3 | using Spectre.Console;
4 | using System.Collections.Concurrent;
5 | using System.Diagnostics;
6 | using System.Threading;
7 | using System;
8 | using System.Linq;
9 |
10 | // Interactive entrypoint: if no args provided, launch interactive menu
11 | if (args.Length == 0)
12 | {
13 | InteractiveMode();
14 | }
15 | else
16 | {
17 | RunWithArgs(args);
18 | }
19 |
20 | void InteractiveMode()
21 | {
22 | AnsiConsole.MarkupLine("[green]=== Miku Interactive Performance Test ===[/]");
23 | var role = AnsiConsole.Prompt(
24 | new SelectionPrompt()
25 | .Title("Select mode:")
26 | .AddChoices("Server", "Client"));
27 | if (role == "Server")
28 | {
29 | var ip = AnsiConsole.Prompt(new TextPrompt("Enter IP:").DefaultValue("127.0.0.1"));
30 | var port = AnsiConsole.Prompt(new TextPrompt("Enter port:").DefaultValue(55550));
31 | var serverBufferSize = AnsiConsole.Prompt(new TextPrompt("Enter buffer size (bytes):").DefaultValue(64 * 1024));
32 | var mode = AnsiConsole.Prompt(
33 | new SelectionPrompt()
34 | .Title("Select server mode:")
35 | .AddChoices("echo", "broadcast", "silent"));
36 | RunWithArgs(new[] { "server", "--ip", ip, "--port", port.ToString(), "--buffersize", serverBufferSize.ToString(), "--mode", mode });
37 | }
38 | else
39 | {
40 | var ip = AnsiConsole.Prompt(new TextPrompt("Enter IP:").DefaultValue("127.0.0.1"));
41 | var port = AnsiConsole.Prompt(new TextPrompt("Enter port:").DefaultValue(55550));
42 | var size = AnsiConsole.Prompt(new TextPrompt("Message size (bytes):").DefaultValue(100));
43 | var rate = AnsiConsole.Prompt(new TextPrompt("Messages per second:").DefaultValue(100));
44 | var duration = AnsiConsole.Prompt(new TextPrompt("Duration (seconds):").DefaultValue(30));
45 | var clientsCount = AnsiConsole.Prompt(new TextPrompt("Number of clients:").DefaultValue(1));
46 | var clientBufferSize = AnsiConsole.Prompt(new TextPrompt("Enter client buffer size (bytes):").DefaultValue(Math.Max(1024, size * 2)));
47 | var mode = AnsiConsole.Prompt(
48 | new SelectionPrompt()
49 | .Title("Select client mode:")
50 | .AddChoices("burst", "sustained", "latency"));
51 | RunWithArgs(new[]
52 | {
53 | "client", "--ip", ip, "--port", port.ToString(), "--buffersize", clientBufferSize.ToString(), "--size", size.ToString(),
54 | "--rate", rate.ToString(), "--duration", duration.ToString(), "--clients", clientsCount.ToString(),
55 | "--mode", mode
56 | });
57 | }
58 | }
59 |
60 | void RunWithArgs(string[] runArgs)
61 | {
62 | var app = ConsoleApp.Create();
63 | app.Add();
64 | app.Add();
65 | app.Run(runArgs);
66 | }
67 |
68 | ///
69 | /// Server commands for performance testing
70 | ///
71 | public class ServerCommands
72 | {
73 | private static NetServer? _server;
74 | private static readonly PerformanceMetrics _metrics = new();
75 | private static readonly ConcurrentDictionary _clients = new();
76 | private static string _mode = "echo";
77 | private static readonly object _consoleLock = new();
78 |
79 | ///
80 | /// Start performance test server
81 | ///
82 | /// IP address to bind to
83 | /// Port to listen on
84 | /// Buffer size per client connection in bytes
85 | /// Server mode: echo, broadcast, silent
86 | public async Task Server(string ip = "127.0.0.1", int port = 55550, int bufferSize = 64 * 1024, string mode = "echo")
87 | {
88 | _mode = mode.ToLower();
89 |
90 | if (_mode is not ("echo" or "broadcast" or "silent"))
91 | {
92 | AnsiConsole.MarkupLine("Invalid mode. Use: echo, broadcast, or silent");
93 | return;
94 | }
95 |
96 | AnsiConsole.MarkupLine("=== Miku Performance Test Server ===");
97 | AnsiConsole.MarkupLine($"IP: {ip}, Port: {port}, Mode: {_mode}");
98 | AnsiConsole.MarkupLine("==========================================");
99 |
100 | try
101 | {
102 | _server = new NetServer();
103 | _server.BufferSize = bufferSize;
104 | SetupServerEvents();
105 |
106 | _server.Start(ip, port);
107 | AnsiConsole.MarkupLine($"Server started on {ip}:{port}");
108 |
109 | // Start live stats display (no runtime commands)
110 | var table = new Table().AddColumn("Metric").AddColumn("Value");
111 | AnsiConsole.Live(table)
112 | .AutoClear(false)
113 | .Start(ctx =>
114 | {
115 | var process = Process.GetCurrentProcess();
116 | var prevCpuTime = process.TotalProcessorTime;
117 | var prevTime = DateTime.UtcNow;
118 | while (true)
119 | {
120 | LiveHelper.PromptAndReset(() => _metrics.Reset());
121 | LiveHelper.UpdateServerRows(table, _metrics, _clients, _mode, ref process, ref prevCpuTime, ref prevTime);
122 | ctx.Refresh();
123 | Thread.Sleep(1000);
124 | }
125 | });
126 | }
127 | catch (Exception ex)
128 | {
129 | AnsiConsole.MarkupLine($"Error: {ex.Message}");
130 | }
131 | finally
132 | {
133 | _server?.Stop();
134 | _server?.Dispose();
135 | AnsiConsole.MarkupLine("Server stopped.");
136 | var (received, sent, receivedPerSec, sentPerSec, receivedMBps, sentMBps, elapsed) = _metrics.GetStats();
137 | AnsiConsole.MarkupLine("=== Final Server Results ===");
138 | AnsiConsole.MarkupLine($"Received: {received:N0}, Sent: {sent:N0}");
139 | AnsiConsole.MarkupLine($"Recv/s: {receivedPerSec:F1}, Sent/s: {sentPerSec:F1}");
140 | AnsiConsole.MarkupLine($"Recv MB/s: {receivedMBps:F2}, Sent MB/s: {sentMBps:F2}");
141 | AnsiConsole.MarkupLine($"Elapsed: {elapsed:c}");
142 | }
143 | await Task.CompletedTask;
144 | }
145 |
146 | private static void SetupServerEvents()
147 | {
148 | _server!.OnClientConnected += client =>
149 | {
150 | _clients[client.Id] = client;
151 | lock (_consoleLock)
152 | {
153 | AnsiConsole.MarkupLine($"Client {client.Id} connected from {client.Ip}. Total clients: {_clients.Count}");
154 | }
155 | };
156 |
157 | _server.OnClientDisconnected += (client, reason) =>
158 | {
159 | _clients.TryRemove(client.Id, out _);
160 | lock (_consoleLock)
161 | {
162 | AnsiConsole.MarkupLine($"Client {client.Id} disconnected: {reason}. Total clients: {_clients.Count}");
163 | }
164 | };
165 |
166 | _server.OnClientDataReceived += (client, data) =>
167 | {
168 | _metrics.RecordMessageReceived(data.Length);
169 |
170 | switch (_mode)
171 | {
172 | case "echo":
173 | client.Send(data);
174 | _metrics.RecordMessageSent(data.Length);
175 | break;
176 |
177 | case "broadcast":
178 | var dataArray = data;
179 | foreach (var connectedClient in _clients.Values)
180 | {
181 | try
182 | {
183 | connectedClient.Send(dataArray);
184 | _metrics.RecordMessageSent(data.Length);
185 | }
186 | catch (Exception ex)
187 | {
188 | lock (_consoleLock)
189 | {
190 | AnsiConsole.MarkupLine($"Error broadcasting to client {connectedClient.Id}: {ex.Message}");
191 | }
192 | }
193 | }
194 | break;
195 |
196 | case "silent":
197 | // Just receive, don't send anything back
198 | break;
199 | }
200 | };
201 |
202 | _server.OnError += ex =>
203 | {
204 | lock (_consoleLock)
205 | {
206 | AnsiConsole.MarkupLine($"Server error: {ex.Message}");
207 | }
208 | };
209 | }
210 | }
211 |
212 | ///
213 | /// Client commands for performance testing
214 | ///
215 | public class ClientCommands
216 | {
217 | private static readonly List _clients = new();
218 | private static readonly ClientPerformanceMetrics _metrics = new();
219 | private static volatile bool _isRunning = true;
220 | private static readonly object _consoleLock = new();
221 | private static readonly ConcurrentQueue _sentTimes = new();
222 |
223 | ///
224 | /// Run performance test client
225 | ///
226 | /// Server IP address
227 | /// Server port
228 | /// Receive buffer size in bytes (should be >= message size)
229 | /// Message size in bytes
230 | /// Messages per second
231 | /// Test duration in seconds
232 | /// Number of concurrent clients
233 | /// Test mode: burst, sustained, latency
234 | public async Task Client(
235 | string ip = "127.0.0.1",
236 | int port = 55550,
237 | int bufferSize = 1024,
238 | int size = 100,
239 | int rate = 100,
240 | int duration = 30,
241 | int clients = 1,
242 | string mode = "burst")
243 | {
244 | mode = mode.ToLower();
245 |
246 | if (mode is not ("burst" or "sustained" or "latency"))
247 | {
248 | AnsiConsole.MarkupLine("Invalid mode. Use: burst, sustained, or latency");
249 | return;
250 | }
251 |
252 | AnsiConsole.MarkupLine("=== Miku Performance Test Client ===");
253 | AnsiConsole.MarkupLine($"Target: {ip}:{port}");
254 | AnsiConsole.MarkupLine($"Receive Buffer: {bufferSize} bytes, Message Size: {size} bytes");
255 | AnsiConsole.MarkupLine($"Rate: {rate} msg/s");
256 | AnsiConsole.MarkupLine($"Duration: {duration} seconds");
257 | AnsiConsole.MarkupLine($"Clients: {clients}");
258 | AnsiConsole.MarkupLine($"Mode: {mode}");
259 | AnsiConsole.MarkupLine("=====================================");
260 |
261 | try
262 | {
263 | // Run the test with live stats display
264 | var testTask = RunTest(ip, port, bufferSize, size, rate, duration, clients, mode);
265 | var table = new Table().AddColumn("Metric").AddColumn("Value");
266 | AnsiConsole.Live(table)
267 | .AutoClear(false)
268 | .Start(ctx =>
269 | {
270 | var process = Process.GetCurrentProcess();
271 | var prevCpuTime = process.TotalProcessorTime;
272 | var prevTime = DateTime.UtcNow;
273 | while (!testTask.IsCompleted)
274 | {
275 | LiveHelper.PromptAndReset(() => _metrics.Reset());
276 | LiveHelper.UpdateClientRows(table, _metrics, _clients, mode, ref process, ref prevCpuTime, ref prevTime);
277 | ctx.Refresh();
278 | Thread.Sleep(1000);
279 | }
280 | });
281 | await testTask;
282 | }
283 | catch (Exception ex)
284 | {
285 | AnsiConsole.MarkupLine($"Error: {ex.Message}");
286 | }
287 | finally
288 | {
289 | await DisconnectAll();
290 | var (sent, received, sentPerSec, receivedPerSec, sentMBps, receivedMBps, avgRoundTripMs, elapsed) = _metrics.GetStats();
291 | AnsiConsole.MarkupLine("=== Final Client Results ===");
292 | AnsiConsole.MarkupLine($"Sent: {sent:N0}, Received: {received:N0}");
293 | AnsiConsole.MarkupLine($"Sent/s: {sentPerSec:F1}, Recv/s: {receivedPerSec:F1}");
294 | AnsiConsole.MarkupLine($"Sent MB/s: {sentMBps:F2}, Recv MB/s: {receivedMBps:F2}");
295 | AnsiConsole.MarkupLine($"Avg RTT (ms): {avgRoundTripMs:F1}");
296 | AnsiConsole.MarkupLine($"Elapsed: {elapsed:c}");
297 | AnsiConsole.MarkupLine("Client test completed.");
298 | }
299 | }
300 |
301 | private static async Task RunTest(string ip, int port, int bufferSize, int messageSize, int messagesPerSecond, int duration, int clientCount, string mode)
302 | {
303 | try
304 | {
305 | _metrics.Reset();
306 |
307 | // Create and connect clients
308 | for (int i = 0; i < clientCount; i++)
309 | {
310 | var client = new NetClient();
311 | SetupClientEvents(client);
312 |
313 | try
314 | {
315 | client.Connect(ip, port, bufferSize);
316 | _clients.Add(client);
317 |
318 | if (i < clientCount - 1)
319 | await Task.Delay(10);
320 | }
321 | catch (Exception ex)
322 | {
323 | AnsiConsole.MarkupLine($"Failed to connect client {i + 1}: {ex.Message}");
324 | client.Dispose();
325 | }
326 | }
327 |
328 | var connectedClients = _clients.Count(c => c.IsConnected);
329 | AnsiConsole.MarkupLine($"Connected {connectedClients}/{clientCount} clients");
330 |
331 | if (connectedClients == 0)
332 | {
333 | AnsiConsole.MarkupLine("No clients connected. Aborting test.");
334 | return;
335 | }
336 |
337 | // Start the appropriate test mode
338 | switch (mode)
339 | {
340 | case "burst":
341 | await RunBurstTest(messageSize, messagesPerSecond, duration);
342 | break;
343 | case "sustained":
344 | await RunSustainedTest(messageSize, messagesPerSecond, duration);
345 | break;
346 | case "latency":
347 | await RunLatencyTest(messageSize, duration);
348 | break;
349 | }
350 | }
351 | catch (Exception ex)
352 | {
353 | AnsiConsole.MarkupLine($"Test error: {ex.Message}");
354 | }
355 | finally
356 | {
357 | await Task.Delay(1000); // Allow final messages to be processed
358 | AnsiConsole.MarkupLine("Test completed.");
359 | }
360 | }
361 |
362 | private static void SetupClientEvents(NetClient client)
363 | {
364 | client.OnConnected += () =>
365 | {
366 | lock (_consoleLock)
367 | {
368 | AnsiConsole.MarkupLine($"Client {client.Id} connected");
369 | }
370 | };
371 |
372 | client.OnDisconnected += reason =>
373 | {
374 | lock (_consoleLock)
375 | {
376 | AnsiConsole.MarkupLine($"Client {client.Id} disconnected: {reason}");
377 | }
378 | };
379 |
380 | client.OnDataReceived += data =>
381 | {
382 | _metrics.RecordMessageReceived(data.Length);
383 |
384 | if (_sentTimes.TryDequeue(out var sentTime))
385 | {
386 | var roundTripMs = (DateTime.UtcNow - sentTime).TotalMilliseconds;
387 | _metrics.RecordRoundTrip((long)roundTripMs);
388 | }
389 | };
390 |
391 | client.OnError += ex =>
392 | {
393 | lock (_consoleLock)
394 | {
395 | AnsiConsole.MarkupLine($"Client {client.Id} error: {ex.Message}");
396 | }
397 | };
398 | }
399 |
400 | private static async Task RunBurstTest(int messageSize, int messagesPerSecond, int duration)
401 | {
402 | AnsiConsole.MarkupLine($"Running burst test: {messagesPerSecond} msg/s for {duration} seconds");
403 |
404 | var payload = new byte[messageSize];
405 | new Random(42).NextBytes(payload);
406 |
407 | var stopwatch = Stopwatch.StartNew();
408 | var endTime = TimeSpan.FromSeconds(duration);
409 | var interval = TimeSpan.FromMilliseconds(1000.0 / messagesPerSecond);
410 | var nextSendTime = stopwatch.Elapsed;
411 |
412 | while (stopwatch.Elapsed < endTime && _isRunning)
413 | {
414 | var now = stopwatch.Elapsed;
415 | if (now < nextSendTime)
416 | {
417 | await Task.Delay(nextSendTime - now);
418 | }
419 |
420 | foreach (var client in _clients.Where(c => c.IsConnected))
421 | {
422 | try
423 | {
424 | _sentTimes.Enqueue(DateTime.UtcNow);
425 | client.Send(payload);
426 | _metrics.RecordMessageSent(messageSize);
427 | }
428 | catch (Exception ex)
429 | {
430 | AnsiConsole.MarkupLine($"Send error on client {client.Id}: {ex.Message}");
431 | }
432 | }
433 |
434 | nextSendTime += interval;
435 | }
436 | }
437 |
438 | private static async Task RunSustainedTest(int messageSize, int messagesPerSecond, int duration)
439 | {
440 | AnsiConsole.MarkupLine($"Running sustained test: {messagesPerSecond} msg/s for {duration} seconds");
441 |
442 | var payload = new byte[messageSize];
443 | new Random(42).NextBytes(payload);
444 |
445 | var totalMessages = messagesPerSecond * duration;
446 | var messagesPerClient = totalMessages / Math.Max(1, _clients.Count(c => c.IsConnected));
447 | var delayBetweenMessages = TimeSpan.FromMilliseconds(1000.0 / messagesPerSecond * _clients.Count(c => c.IsConnected));
448 |
449 | var tasks = _clients.Where(c => c.IsConnected).Select(async client =>
450 | {
451 | for (int i = 0; i < messagesPerClient && _isRunning; i++)
452 | {
453 | try
454 | {
455 | _sentTimes.Enqueue(DateTime.UtcNow);
456 | client.Send(payload);
457 | _metrics.RecordMessageSent(messageSize);
458 |
459 | if (i < messagesPerClient - 1)
460 | await Task.Delay(delayBetweenMessages);
461 | }
462 | catch (Exception ex)
463 | {
464 | AnsiConsole.MarkupLine($"Send error on client {client.Id}: {ex.Message}");
465 | break;
466 | }
467 | }
468 | }).ToArray();
469 |
470 | await Task.WhenAll(tasks);
471 | }
472 |
473 | private static async Task RunLatencyTest(int messageSize, int duration)
474 | {
475 | AnsiConsole.MarkupLine($"Running latency test: ping-pong for {duration} seconds");
476 |
477 | var payload = new byte[messageSize];
478 | new Random(42).NextBytes(payload);
479 |
480 | if (!_clients.Any(c => c.IsConnected))
481 | {
482 | AnsiConsole.MarkupLine("No connected clients for latency test");
483 | return;
484 | }
485 |
486 | var client = _clients.First(c => c.IsConnected);
487 | var stopwatch = Stopwatch.StartNew();
488 | var endTime = TimeSpan.FromSeconds(duration);
489 |
490 | while (stopwatch.Elapsed < endTime && _isRunning)
491 | {
492 | try
493 | {
494 | _sentTimes.Enqueue(DateTime.UtcNow);
495 | client.Send(payload);
496 | _metrics.RecordMessageSent(messageSize);
497 |
498 | await Task.Delay(10);
499 | }
500 | catch (Exception ex)
501 | {
502 | AnsiConsole.MarkupLine($"Latency test error: {ex.Message}");
503 | break;
504 | }
505 | }
506 | }
507 |
508 | private static async Task DisconnectAll()
509 | {
510 | var disconnectTasks = _clients.Select(client =>
511 | {
512 | try
513 | {
514 | if (client.IsConnected)
515 | {
516 | client.Stop();
517 | }
518 | client.Dispose();
519 | }
520 | catch (Exception ex)
521 | {
522 | AnsiConsole.MarkupLine($"Error disconnecting client {client.Id}: {ex.Message}");
523 | }
524 | return Task.CompletedTask;
525 | }).ToArray();
526 |
527 | await Task.WhenAll(disconnectTasks);
528 | _clients.Clear();
529 | }
530 | }
531 |
532 | ///
533 | /// Performance metrics tracking
534 | ///
535 | public class PerformanceMetrics
536 | {
537 | private long _messagesReceived;
538 | private long _messagesSent;
539 | private long _bytesReceived;
540 | private long _bytesSent;
541 | private readonly object _lock = new();
542 | private DateTime _startTime = DateTime.UtcNow;
543 |
544 | public void RecordMessageReceived(int bytes)
545 | {
546 | Interlocked.Increment(ref _messagesReceived);
547 | Interlocked.Add(ref _bytesReceived, bytes);
548 | }
549 |
550 | public void RecordMessageSent(int bytes)
551 | {
552 | Interlocked.Increment(ref _messagesSent);
553 | Interlocked.Add(ref _bytesSent, bytes);
554 | }
555 |
556 | public void Reset()
557 | {
558 | lock (_lock)
559 | {
560 | _messagesReceived = 0;
561 | _messagesSent = 0;
562 | _bytesReceived = 0;
563 | _bytesSent = 0;
564 | _startTime = DateTime.UtcNow;
565 | }
566 | }
567 |
568 | public (long received, long sent, double receivedPerSec, double sentPerSec, double receivedMBps, double sentMBps, TimeSpan elapsed) GetStats()
569 | {
570 | lock (_lock)
571 | {
572 | var elapsed = DateTime.UtcNow - _startTime;
573 | var totalSeconds = elapsed.TotalSeconds;
574 |
575 | var receivedPerSec = totalSeconds > 0 ? _messagesReceived / totalSeconds : 0;
576 | var sentPerSec = totalSeconds > 0 ? _messagesSent / totalSeconds : 0;
577 | var receivedMBps = totalSeconds > 0 ? (_bytesReceived / 1024.0 / 1024.0) / totalSeconds : 0;
578 | var sentMBps = totalSeconds > 0 ? (_bytesSent / 1024.0 / 1024.0) / totalSeconds : 0;
579 |
580 | return (_messagesReceived, _messagesSent, receivedPerSec, sentPerSec, receivedMBps, sentMBps, elapsed);
581 | }
582 | }
583 | }
584 |
585 | ///
586 | /// Client-specific performance metrics
587 | ///
588 | public class ClientPerformanceMetrics
589 | {
590 | private long _messagesSent;
591 | private long _messagesReceived;
592 | private long _bytesSent;
593 | private long _bytesReceived;
594 | private long _roundTripCount;
595 | private long _totalRoundTripMs;
596 | private readonly object _lock = new();
597 | private DateTime _startTime = DateTime.UtcNow;
598 |
599 | public void RecordMessageSent(int bytes)
600 | {
601 | Interlocked.Increment(ref _messagesSent);
602 | Interlocked.Add(ref _bytesSent, bytes);
603 | }
604 |
605 | public void RecordMessageReceived(int bytes)
606 | {
607 | Interlocked.Increment(ref _messagesReceived);
608 | Interlocked.Add(ref _bytesReceived, bytes);
609 | }
610 |
611 | public void RecordRoundTrip(long milliseconds)
612 | {
613 | Interlocked.Increment(ref _roundTripCount);
614 | Interlocked.Add(ref _totalRoundTripMs, milliseconds);
615 | }
616 |
617 | public void Reset()
618 | {
619 | lock (_lock)
620 | {
621 | _messagesSent = 0;
622 | _messagesReceived = 0;
623 | _bytesSent = 0;
624 | _bytesReceived = 0;
625 | _roundTripCount = 0;
626 | _totalRoundTripMs = 0;
627 | _startTime = DateTime.UtcNow;
628 | }
629 | }
630 |
631 | public (long sent, long received, double sentPerSec, double receivedPerSec, double sentMBps, double receivedMBps, double avgRoundTripMs, TimeSpan elapsed) GetStats()
632 | {
633 | lock (_lock)
634 | {
635 | var elapsed = DateTime.UtcNow - _startTime;
636 | var totalSeconds = elapsed.TotalSeconds;
637 |
638 | var sentPerSec = totalSeconds > 0 ? _messagesSent / totalSeconds : 0;
639 | var receivedPerSec = totalSeconds > 0 ? _messagesReceived / totalSeconds : 0;
640 | var sentMBps = totalSeconds > 0 ? (_bytesSent / 1024.0 / 1024.0) / totalSeconds : 0;
641 | var receivedMBps = totalSeconds > 0 ? (_bytesReceived / 1024.0 / 1024.0) / totalSeconds : 0;
642 | var avgRoundTripMs = _roundTripCount > 0 ? (double)_totalRoundTripMs / _roundTripCount : 0;
643 |
644 | return (_messagesSent, _messagesReceived, sentPerSec, receivedPerSec, sentMBps, receivedMBps, avgRoundTripMs, elapsed);
645 | }
646 | }
647 | }
648 |
649 | public static class LiveHelper
650 | {
651 | public static void PromptAndReset(Action resetAction)
652 | {
653 | if (!Console.KeyAvailable) return;
654 | var keyInfo = Console.ReadKey(true);
655 | if (keyInfo.Key == ConsoleKey.R) resetAction();
656 | }
657 |
658 | public static void AddSystemRows(Table table, ref Process process, ref TimeSpan prevCpuTime, ref DateTime prevTime)
659 | {
660 | process.Refresh();
661 | var curCpuTime = process.TotalProcessorTime;
662 | var curTime = DateTime.UtcNow;
663 | var cpuUsage = (curCpuTime - prevCpuTime).TotalMilliseconds
664 | / ((curTime - prevTime).TotalMilliseconds * Environment.ProcessorCount) * 100;
665 | prevCpuTime = curCpuTime;
666 | prevTime = curTime;
667 | var memoryMb = process.WorkingSet64 / 1024.0 / 1024.0;
668 |
669 | table.AddRow("CPU (%)", cpuUsage.ToString("F1"));
670 | table.AddRow("Memory (MB)", memoryMb.ToString("F2"));
671 | table.AddRow("Press 'R' to reset", string.Empty);
672 | }
673 |
674 | public static void UpdateServerRows(Table table, PerformanceMetrics metrics, ConcurrentDictionary clients, string mode, ref Process process, ref TimeSpan prevCpuTime, ref DateTime prevTime)
675 | {
676 | var (received, sent, receivedPerSec, sentPerSec, receivedMBps, sentMBps, elapsed) = metrics.GetStats();
677 | table.Rows.Clear();
678 | table.AddRow("Received", received.ToString("N0"));
679 | table.AddRow("Sent", sent.ToString("N0"));
680 | table.AddRow("Recv/s", receivedPerSec.ToString("F1"));
681 | table.AddRow("Sent/s", sentPerSec.ToString("F1"));
682 | table.AddRow("Recv MB/s", receivedMBps.ToString("F2"));
683 | table.AddRow("Sent MB/s", sentMBps.ToString("F2"));
684 | table.AddRow("Clients", clients.Count.ToString());
685 | table.AddRow("Mode", mode);
686 | table.AddRow("Uptime", elapsed.ToString(@"hh\:mm\:ss"));
687 | AddSystemRows(table, ref process, ref prevCpuTime, ref prevTime);
688 | }
689 |
690 | public static void UpdateClientRows(Table table, ClientPerformanceMetrics metrics, List clients, string mode, ref Process process, ref TimeSpan prevCpuTime, ref DateTime prevTime)
691 | {
692 | var (sent, received, sentPerSec, receivedPerSec, sentMBps, receivedMBps, avgRoundTripMs, elapsed) = metrics.GetStats();
693 | table.Rows.Clear();
694 | table.AddRow("Sent", sent.ToString("N0"));
695 | table.AddRow("Received", received.ToString("N0"));
696 | table.AddRow("Sent/s", sentPerSec.ToString("F1"));
697 | table.AddRow("Recv/s", receivedPerSec.ToString("F1"));
698 | table.AddRow("Sent MB/s", sentMBps.ToString("F2"));
699 | table.AddRow("Recv MB/s", receivedMBps.ToString("F2"));
700 | table.AddRow("RT(ms)", avgRoundTripMs.ToString("F1"));
701 | table.AddRow("Clients", clients.Count(c => c.IsConnected).ToString());
702 | table.AddRow("Mode", mode);
703 | table.AddRow("Elapsed", elapsed.ToString(@"hh\:mm\:ss"));
704 | AddSystemRows(table, ref process, ref prevCpuTime, ref prevTime);
705 | }
706 | }
707 |
--------------------------------------------------------------------------------
/Miku.Core/NetClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.Threading;
4 | using System.Net.Sockets;
5 | using System.Collections.Generic;
6 | using System.Runtime.InteropServices;
7 | using System.Text;
8 | using System.Collections.Concurrent;
9 | using System.Threading.Channels;
10 | using System.Diagnostics;
11 |
12 | namespace Miku.Core
13 | {
14 | ///
15 | /// Performance statistics for NetClient to track optimization effectiveness
16 | ///
17 | public struct NetClientStats
18 | {
19 | public long MessagesReceived;
20 | public long MessagesSent;
21 | public long BytesReceived;
22 | public long BytesSent;
23 | public long OutgoingMessagesDropped; // Only outgoing messages can be dropped (channel full)
24 | public long ErrorsSkipped; // Errors skipped due to null OnError handler
25 | public long TempBufferAllocations; // Temp allocations for segmented data
26 |
27 | ///
28 | /// Drop rate for outgoing messages only (incoming messages use direct callbacks - no drops)
29 | ///
30 | public readonly double OutgoingDropRate =>
31 | MessagesSent > 0 ? (double)OutgoingMessagesDropped / (MessagesSent + OutgoingMessagesDropped) : 0;
32 |
33 | public readonly double ErrorSkipRate =>
34 | ErrorsSkipped > 0 ? (double)ErrorsSkipped / (ErrorsSkipped + MessagesReceived) : 0;
35 |
36 | ///
37 | /// Reset all counters to prevent overflow when approaching long.MaxValue
38 | /// Thread-safe via Interlocked.Exchange.
39 | ///
40 | public void ResetIfNearOverflow()
41 | {
42 | const long resetThreshold = (long)(long.MaxValue * 0.9);
43 |
44 | if (MessagesReceived > resetThreshold || MessagesSent > resetThreshold ||
45 | BytesReceived > resetThreshold || BytesSent > resetThreshold)
46 | {
47 | Interlocked.Exchange(ref MessagesReceived, 0);
48 | Interlocked.Exchange(ref MessagesSent, 0);
49 | Interlocked.Exchange(ref BytesReceived, 0);
50 | Interlocked.Exchange(ref BytesSent, 0);
51 | Interlocked.Exchange(ref OutgoingMessagesDropped, 0);
52 | Interlocked.Exchange(ref ErrorsSkipped, 0);
53 | Interlocked.Exchange(ref TempBufferAllocations, 0);
54 | }
55 | }
56 | }
57 |
58 | ///
59 | /// Raw message data for the outgoing channel with its buffer writer
60 | ///
61 | internal readonly struct RawMessage
62 | {
63 | public readonly ArrayBufferWriter BufferWriter;
64 | public readonly int Length;
65 |
66 | public RawMessage(ArrayBufferWriter bufferWriter, int length)
67 | {
68 | BufferWriter = bufferWriter;
69 | Length = length;
70 | }
71 | }
72 |
73 | ///
74 | /// Configuration options for NetClient performance tuning
75 | ///
76 | public class NetClientConfig
77 | {
78 | ///
79 | /// Number of messages to process before yielding thread
80 | ///
81 | public int YieldThreshold { get; set; } = 5;
82 |
83 | ///
84 | /// Number of messages to batch before updating stats
85 | ///
86 | public int BatchSize { get; set; } = 32;
87 |
88 | ///
89 | /// Channel capacity for outgoing messages
90 | ///
91 | public int OutgoingChannelCapacity { get; set; } = 10000;
92 | }
93 |
94 | ///
95 | /// A client for connecting to a remote host with channel-based data flow
96 | ///
97 | public class NetClient : IDisposable
98 | {
99 | ///
100 | /// Shared pool for ArrayBufferWriter instances used across all clients
101 | ///
102 | private static readonly ConcurrentQueue> BufferPool = new();
103 |
104 | ///
105 | /// Default configuration
106 | ///
107 | private static readonly NetClientConfig DefaultConfig = new();
108 |
109 | ///
110 | /// Static counter for generating unique client IDs
111 | ///
112 | private static int _nextId = 0;
113 |
114 | ///
115 | /// Generate next unique client ID with overflow handling
116 | ///
117 | private static int GetNextId()
118 | {
119 | int newId;
120 | int current;
121 | do
122 | {
123 | current = _nextId;
124 | newId = current == int.MaxValue ? 0 : current + 1; // Wrap to 0 on overflow
125 | } while (Interlocked.CompareExchange(ref _nextId, newId, current) != current);
126 |
127 | return newId;
128 | }
129 |
130 | ///
131 | /// Event when the client is connected
132 | ///
133 | public event Action OnConnected;
134 |
135 | ///
136 | /// Event when the client is disconnected
137 | ///
138 | public event Action OnDisconnected;
139 |
140 | ///
141 | /// Event when data is received (called from Dispatch)
142 | ///
143 | public event Action> OnDataReceived;
144 |
145 | ///
146 | /// Event when an error occurred
147 | ///
148 | public event Action OnError;
149 |
150 | ///
151 | /// Unique identifier for the client
152 | ///
153 | public int Id { get; } = GetNextId();
154 |
155 | ///
156 | /// Whether the client is connected
157 | ///
158 | public bool IsConnected
159 | {
160 | get
161 | {
162 | if (_disposed != 0 || !_isConnected) return false;
163 |
164 | try
165 | {
166 | var socket = _socket; // Capture reference to prevent null reference
167 | return socket?.Connected == true;
168 | }
169 | catch
170 | {
171 | return false;
172 | }
173 | }
174 | }
175 |
176 | ///
177 | /// Remote host IP address
178 | ///
179 | public string Ip { get; private set; }
180 |
181 | public int BufferSize => _bufferSize;
182 |
183 | ///
184 | /// Performance statistics for monitoring optimization effectiveness
185 | ///
186 | public NetClientStats Stats => _stats;
187 |
188 | ///
189 | /// Manually reset performance statistics
190 | ///
191 | public void ResetStats()
192 | {
193 | _stats = default;
194 | }
195 |
196 | private Socket _socket;
197 | private bool _isConnected;
198 | private int _sending;
199 | private volatile int _bufferSize;
200 |
201 | private SocketAsyncEventArgs _receiveArg;
202 | private SocketAsyncEventArgs _sendArg;
203 | private NetBuffer _receiveBuffer;
204 | private readonly List _middlewares = new();
205 | private ArrayBufferWriter _currentBufferWriter;
206 |
207 | private readonly ArraySegment[]
208 | _saeaBufferList = new ArraySegment[2]; // Fixed-size array to avoid allocations
209 |
210 | private Channel _outgoingChannel;
211 |
212 | private StringBuilder _diagnosticInfoBuilder = new();
213 |
214 | // Pre-allocated exception for performance
215 | private static readonly InvalidOperationException CachedReceiveBufferNullException =
216 | new("Receive buffer is null");
217 |
218 | // Bounding the pool prevents it from growing indefinitely, which can cause
219 | // memory pressure in long-running processes.
220 | private const int MaxPooledWriters = 1_024;
221 |
222 | ///
223 | /// Get a buffer writer from the pool or create a new one
224 | ///
225 | private static ArrayBufferWriter GetBufferFromPool() =>
226 | BufferPool.TryDequeue(out var writer) ? writer : new ArrayBufferWriter();
227 |
228 | ///
229 | /// Return a buffer writer to the pool
230 | ///
231 | private static void ReturnBufferToPool(ArrayBufferWriter writer)
232 | {
233 | if (writer == null) return;
234 | try
235 | {
236 | #if NET8_0_OR_GREATER
237 | writer.ResetWrittenCount();
238 | #else
239 | writer.Clear();
240 | #endif
241 | // ConcurrentQueue.Count is O(N), but this is an acceptable trade-off
242 | // for safety, as it's called less frequently than enqueueing.
243 | if (BufferPool.Count >= MaxPooledWriters)
244 | return;
245 |
246 | BufferPool.Enqueue(writer);
247 | }
248 | catch
249 | {
250 | // If returning to the pool fails, let it be garbage collected.
251 | }
252 | }
253 |
254 | ///
255 | /// Resets a buffer writer to be reused.
256 | ///
257 | private static void ResetBuffer(ArrayBufferWriter writer)
258 | {
259 | #if NET8_0_OR_GREATER
260 | writer.ResetWrittenCount();
261 | #else
262 | writer.Clear();
263 | #endif
264 | }
265 |
266 | ///
267 | /// Whether there are pending messages to send
268 | ///
269 | public bool HasPendingSends
270 | {
271 | get
272 | {
273 | try
274 | {
275 | return _outgoingChannel?.Reader.TryPeek(out _) == true;
276 | }
277 | catch (InvalidOperationException)
278 | {
279 | // Channel completed
280 | return false;
281 | }
282 | }
283 | }
284 |
285 | ///
286 | /// Count of pending messages to send
287 | ///
288 | public int PendingSendCount
289 | {
290 | get
291 | {
292 | try
293 | {
294 | return _outgoingChannel?.Reader.Count ?? 0;
295 | }
296 | catch (InvalidOperationException)
297 | {
298 | // Channel completed
299 | return 0;
300 | }
301 | }
302 | }
303 |
304 | private int _stopping;
305 | private int _inErrorHandler; // To prevent reentrancy in error handling
306 | private int _disposed;
307 | private bool _isZeroByteReceive;
308 |
309 | // Performance tracking
310 | private NetClientStats _stats;
311 |
312 | // Cached values for optimization
313 | private int _yieldCounter;
314 | private int _overflowCheckCounter;
315 | private int _localMessagesReceived;
316 | private long _localBytesReceived;
317 | private const int OverflowCheckFrequency = 10000;
318 |
319 | ///
320 | /// Initialize the client with the given socket and buffer size
321 | ///
322 | private void InitializeClient(Socket socket, int bufferSize)
323 | {
324 | _bufferSize = bufferSize;
325 | _socket = socket;
326 |
327 | var remoteEndPoint = socket.RemoteEndPoint as System.Net.IPEndPoint;
328 | Ip = remoteEndPoint?.Address.ToString() ?? "Unknown";
329 |
330 | _receiveBuffer = new(bufferSize);
331 |
332 | _receiveArg = new SocketAsyncEventArgs();
333 | _receiveArg.UserToken = this;
334 | _receiveArg.Completed += HandleReadWrite;
335 |
336 | // _saeaBufferList is already allocated as a field.
337 | _sendArg = new SocketAsyncEventArgs();
338 | _sendArg.UserToken = this;
339 | _sendArg.Completed += HandleReadWrite;
340 |
341 | var outgoingOptions = new BoundedChannelOptions(DefaultConfig.OutgoingChannelCapacity)
342 | {
343 | FullMode = BoundedChannelFullMode.DropWrite, // Drop new messages when the channel is full.
344 | SingleReader = true,
345 | SingleWriter = false
346 | };
347 | _outgoingChannel = Channel.CreateBounded(outgoingOptions);
348 |
349 | // Register warning handler after initialization.
350 | _receiveBuffer.OnWarning += msg => SafeInvokeOnError(new Exception(msg));
351 |
352 | // Set _isConnected only after all initialization is complete.
353 | _isConnected = true;
354 |
355 | OnConnected?.Invoke();
356 |
357 | // Start the first receive.
358 | TryStartReceive(_receiveArg);
359 | }
360 |
361 | ///
362 | /// Initiates an asynchronous receive operation with error handling.
363 | ///
364 | private void StartReceiveAsync(SocketAsyncEventArgs args)
365 | {
366 | try
367 | {
368 | if (!_socket.ReceiveAsync(args))
369 | {
370 | Receive(args);
371 | }
372 | }
373 | catch (ObjectDisposedException)
374 | {
375 | // Socket was disposed, so we can ignore this.
376 | }
377 | catch (Exception e)
378 | {
379 | SafeInvokeOnError(e);
380 | }
381 | }
382 |
383 | ///
384 | /// Try to start a receive operation, handling buffer space gracefully
385 | ///
386 | private void TryStartReceive(SocketAsyncEventArgs args)
387 | {
388 | // Cache state to avoid multiple volatile reads
389 | var isDisposed = _disposed != 0;
390 | var isConnected = _isConnected;
391 | var socket = _socket;
392 | var receiveBuffer = _receiveBuffer;
393 |
394 | if (isDisposed || !isConnected || socket == null || receiveBuffer == null)
395 | return;
396 |
397 | // Always process existing buffered data first to prevent deadlocks.
398 | if (receiveBuffer.Length > 0)
399 | {
400 | try
401 | {
402 | ProcessReceivedData();
403 | }
404 | catch (Exception e)
405 | {
406 | SafeInvokeOnError(e);
407 | }
408 | }
409 |
410 | // Re-check state, as it may have changed.
411 | if (_disposed != 0 || !_isConnected || _socket == null)
412 | return;
413 |
414 | if (SetupReceiveBufferList())
415 | {
416 | _isZeroByteReceive = false;
417 | StartReceiveAsync(args);
418 | }
419 | else
420 | {
421 | // Post a zero-byte receive to detect disconnections.
422 | _isZeroByteReceive = true;
423 | args.BufferList = null;
424 | args.SetBuffer(Array.Empty(), 0, 0);
425 | StartReceiveAsync(args);
426 | }
427 | }
428 |
429 | ///
430 | /// Setup BufferList for receiving with fixed-size array (zero-alloc)
431 | ///
432 | /// True if buffer space is available, false if no space
433 | private bool SetupReceiveBufferList()
434 | {
435 | if (_disposed != 0 || !_isConnected || _receiveBuffer == null)
436 | return false;
437 |
438 | var (first, second) = _receiveBuffer.GetWriteSegmentsAsArraySegments();
439 | if ((first.Count | second.Count) == 0)
440 | {
441 | return false;
442 | }
443 |
444 | // Update fixed-size array in-place to avoid allocations.
445 | _saeaBufferList[0] = first;
446 | _saeaBufferList[1] = second;
447 |
448 | // Use BufferList for the receive operation.
449 | _receiveArg.SetBuffer(null, 0, 0);
450 | _receiveArg.BufferList = _saeaBufferList;
451 |
452 | return true;
453 | }
454 |
455 | ///
456 | /// Cleanup resources on connection failure
457 | ///
458 | private void CleanupOnConnectionFailure()
459 | {
460 | _isConnected = false;
461 | _bufferSize = 0;
462 | _socket?.Close();
463 | _socket?.Dispose();
464 | _socket = null;
465 |
466 | _receiveBuffer?.Dispose();
467 | _receiveBuffer = null;
468 |
469 | _receiveArg?.Dispose();
470 | _receiveArg = null;
471 |
472 | _sendArg?.Dispose();
473 | _sendArg = null;
474 |
475 | // _saeaBufferList is a field, no need to clear it.
476 | _outgoingChannel?.Writer.TryComplete();
477 | _outgoingChannel = null;
478 | }
479 |
480 | ///
481 | /// Safely invoke OnError without causing reentrancy issues
482 | ///
483 | private void SafeInvokeOnError(Exception exception)
484 | {
485 | // Prevent reentrancy - if we're already in an error handler, don't invoke again
486 | if (Interlocked.CompareExchange(ref _inErrorHandler, 1, 0) == 1)
487 | {
488 | return;
489 | }
490 |
491 | try
492 | {
493 | OnError?.Invoke(exception);
494 | }
495 | catch
496 | {
497 | // Ignore exceptions from error handlers to prevent loops.
498 | }
499 | finally
500 | {
501 | Interlocked.Exchange(ref _inErrorHandler, 0);
502 | }
503 | }
504 |
505 | ///
506 | /// Add a middleware to the client
507 | ///
508 | ///
509 | public void AddMiddleware(INetMiddleware middleware)
510 | {
511 | _middlewares.Add(middleware);
512 | }
513 |
514 | ///
515 | /// Remove a middleware from the client
516 | ///
517 | ///
518 | public void RemoveMiddleware(INetMiddleware middleware)
519 | {
520 | _middlewares.Remove(middleware);
521 | }
522 |
523 | ///
524 | /// Connect to the remote host
525 | ///
526 | ///
527 | ///
528 | ///
529 | ///
530 | public void Connect(string ip, int port, int bufferSize = 1024)
531 | {
532 | if (_disposed != 0)
533 | {
534 | throw new ObjectDisposedException(nameof(NetClient));
535 | }
536 |
537 | if (_isConnected)
538 | {
539 | throw new InvalidOperationException("Already connected");
540 | }
541 |
542 | if (string.IsNullOrWhiteSpace(ip))
543 | {
544 | throw new ArgumentException("IP address cannot be null or empty", nameof(ip));
545 | }
546 |
547 | if (port <= 0 || port > 65535)
548 | {
549 | throw new ArgumentException("Port must be between 1 and 65535", nameof(port));
550 | }
551 |
552 | // This is important on macOS, where sockets can become invalid after failed connections.
553 | if (_socket != null)
554 | {
555 | try
556 | {
557 | _socket.Close();
558 | _socket.Dispose();
559 | }
560 | catch
561 | {
562 | // Ignore cleanup errors.
563 | }
564 | _socket = null;
565 | }
566 |
567 | try
568 | {
569 | _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
570 |
571 | _socket.NoDelay = true;
572 | _socket.Connect(ip, port);
573 |
574 | InitializeClient(_socket, bufferSize);
575 | Ip = ip;
576 | }
577 | catch (Exception e)
578 | {
579 | CleanupOnConnectionFailure();
580 | SafeInvokeOnError(e);
581 | throw;
582 | }
583 | }
584 |
585 | ///
586 | /// When a server accepts a connection, use this method to connect the client
587 | ///
588 | ///
589 | ///
590 | ///
591 | internal void Connect(Socket socket, int bufferSize = 1024)
592 | {
593 | if (_disposed != 0)
594 | {
595 | throw new ObjectDisposedException(nameof(NetClient));
596 | }
597 |
598 | if (_isConnected)
599 | {
600 | throw new InvalidOperationException("Already connected");
601 | }
602 |
603 | if (socket == null)
604 | {
605 | throw new ArgumentNullException(nameof(socket));
606 | }
607 |
608 | if (!socket.Connected)
609 | {
610 | throw new ArgumentException("Socket must be connected", nameof(socket));
611 | }
612 |
613 | try
614 | {
615 | socket.NoDelay = true;
616 | InitializeClient(socket, bufferSize);
617 | }
618 | catch (Exception e)
619 | {
620 | // Clean up, but don't dispose the socket as we didn't create it.
621 | _isConnected = false;
622 | _bufferSize = 0;
623 | _socket = null;
624 |
625 | _receiveBuffer?.Dispose();
626 | _receiveBuffer = null;
627 |
628 | _receiveArg?.Dispose();
629 | _receiveArg = null;
630 |
631 | _sendArg?.Dispose();
632 | _sendArg = null;
633 |
634 | _outgoingChannel?.Writer.TryComplete();
635 | _outgoingChannel = null;
636 |
637 | SafeInvokeOnError(e);
638 | throw;
639 | }
640 | }
641 |
642 | ///
643 | /// Stop the client with a specific reason
644 | ///
645 | /// Reason for closing the connection
646 | /// Whether to include call stack information (expensive)
647 | public void Stop(string reason = "Connection closed by client", bool includeCallStack = false)
648 | {
649 | string fullReason = reason;
650 |
651 | // Only generate expensive stack trace when requested.
652 | if (includeCallStack)
653 | {
654 | try
655 | {
656 | var stackTrace = new StackTrace(true);
657 | var callStack = new List();
658 | for (int i = 1; i < Math.Min(stackTrace.FrameCount, 10); i++) // Limit to 10 frames
659 | {
660 | var frame = stackTrace.GetFrame(i);
661 | var method = frame?.GetMethod();
662 | if (method != null)
663 | {
664 | var className = method.DeclaringType?.Name ?? "Unknown";
665 | var methodName = method.Name;
666 | callStack.Add($"{className}.{methodName}");
667 | }
668 | }
669 |
670 | fullReason = $"{reason} (called from {string.Join(" -> ", callStack)})";
671 | }
672 | catch
673 | {
674 | // If stack trace fails, just use the original reason.
675 | }
676 | }
677 |
678 | StopInternal(fullReason);
679 | }
680 |
681 | ///
682 | /// Drains and cleans up the outgoing message channel.
683 | ///
684 | private void CleanupOutgoingChannel()
685 | {
686 | if (_outgoingChannel == null) return;
687 | try
688 | {
689 | // This signals that no more items will be written to the channel.
690 | _outgoingChannel.Writer.TryComplete();
691 |
692 | // Drain any remaining messages in the channel.
693 | while (_outgoingChannel.Reader.TryRead(out var outgoingMessage))
694 | {
695 | if (outgoingMessage.BufferWriter != null)
696 | {
697 | ReturnBufferToPool(outgoingMessage.BufferWriter);
698 | }
699 | }
700 | }
701 | catch (InvalidOperationException)
702 | {
703 | // Channel may have been completed and emptied by another thread.
704 | }
705 | catch (Exception e)
706 | {
707 | // Log other exceptions, as they might indicate a problem.
708 | SafeInvokeOnError(e);
709 | }
710 | }
711 |
712 | ///
713 | /// Fast stop for high-frequency scenarios to avoid string allocations.
714 | ///
715 | /// Socket error code or exception type
716 | /// Context where the error occurred
717 | public void StopFast(int errorCode, string context)
718 | {
719 | var reason = $"{context}: {errorCode}";
720 | StopInternal(reason);
721 | }
722 |
723 | ///
724 | /// Internal stop implementation to avoid code duplication
725 | ///
726 | private void StopInternal(string fullReason)
727 | {
728 | if (Interlocked.CompareExchange(ref _stopping, 1, 0) == 1)
729 | {
730 | return;
731 | }
732 |
733 | try
734 | {
735 | if (!_isConnected)
736 | {
737 | CleanupOutgoingChannel();
738 | return;
739 | }
740 |
741 | _isConnected = false;
742 |
743 | try
744 | {
745 | _socket?.Shutdown(SocketShutdown.Both);
746 | }
747 | catch (Exception e)
748 | {
749 | SafeInvokeOnError(e);
750 | }
751 |
752 | _socket?.Close();
753 | _socket?.Dispose();
754 | _socket = null;
755 |
756 | // Complete the channel writer before waiting for sends to prevent new messages.
757 | _outgoingChannel?.Writer.TryComplete();
758 |
759 | // Wait for pending sends to complete.
760 | if (!SpinWait.SpinUntil(() => _sending == 0, TimeSpan.FromMilliseconds(100)))
761 | {
762 | // Force reset on timeout.
763 | Interlocked.Exchange(ref _sending, 0);
764 | }
765 |
766 | // Clean up the current buffer writer.
767 | var currentBufferWriter = Interlocked.Exchange(ref _currentBufferWriter, null);
768 | if (currentBufferWriter != null)
769 | {
770 | ReturnBufferToPool(currentBufferWriter);
771 | }
772 |
773 | // Clean up remaining messages in channels.
774 | CleanupOutgoingChannel();
775 |
776 | _receiveArg?.Dispose();
777 | _sendArg?.Dispose();
778 | _receiveBuffer?.Dispose();
779 |
780 | _receiveArg = null;
781 | _sendArg = null;
782 | _receiveBuffer = null;
783 |
784 | _outgoingChannel = null;
785 |
786 | _bufferSize = 0;
787 |
788 | try
789 | {
790 | // Clear event handlers before invoking OnDisconnected to prevent memory leaks.
791 | var onDisconnectedHandler = OnDisconnected;
792 |
793 | OnConnected = null;
794 | OnDisconnected = null;
795 | OnDataReceived = null;
796 | OnError = null;
797 |
798 | onDisconnectedHandler?.Invoke(fullReason);
799 | }
800 | catch (Exception e)
801 | {
802 | SafeInvokeOnError(e);
803 | }
804 | }
805 | finally
806 | {
807 | Interlocked.Exchange(ref _stopping, 0);
808 | }
809 | }
810 |
811 | ///
812 | /// Send data to the remote host via outgoing channel
813 | ///
814 | ///
815 | /// Whether the data was queued successfully
816 | public void Send(ReadOnlyMemory data)
817 | {
818 | if (Volatile.Read(ref _disposed) != 0 || !Volatile.Read(ref _isConnected) || Volatile.Read(ref _stopping) != 0)
819 | {
820 | return;
821 | }
822 |
823 | var bufferSize = _bufferSize;
824 | if (bufferSize == 0)
825 | {
826 | return;
827 | }
828 |
829 | ArrayBufferWriter buffer1 = GetBufferFromPool();
830 | ArrayBufferWriter buffer2 = null;
831 |
832 | ResetBuffer(buffer1);
833 |
834 | var span = buffer1.GetSpan(data.Length);
835 | data.Span.CopyTo(span);
836 | buffer1.Advance(data.Length);
837 |
838 | try
839 | {
840 | if (_middlewares.Count > 0)
841 | {
842 | buffer2 = GetBufferFromPool();
843 |
844 | ResetBuffer(buffer2);
845 | var src = buffer1;
846 | var dst = buffer2;
847 |
848 | foreach (var middleware in _middlewares)
849 | {
850 | // Process using current src (input) -> dst (output).
851 | middleware.ProcessSend(src.WrittenMemory, dst);
852 |
853 | // The dst buffer is now the src for the next iteration.
854 | (src, dst) = (dst, src);
855 |
856 | // Clear the new dst buffer.
857 | ResetBuffer(dst);
858 | }
859 |
860 | // `src` holds the final data, `dst` is temporary.
861 | buffer1 = src;
862 | ReturnBufferToPool(dst);
863 | buffer2 = null; // Prevent double-return in `finally`.
864 | }
865 |
866 | data = buffer1.WrittenMemory;
867 |
868 | if (data.Length > bufferSize)
869 | {
870 | SafeInvokeOnError(new ArgumentException(
871 | $"Send data too large: {data.Length} bytes (max: {bufferSize})"));
872 | return;
873 | }
874 |
875 | if (data.IsEmpty)
876 | {
877 | return;
878 | }
879 |
880 | try
881 | {
882 | var rawMessage = new RawMessage(buffer1, data.Length);
883 |
884 | if (_outgoingChannel?.Writer.TryWrite(rawMessage) != true)
885 | {
886 | ReturnBufferToPool(buffer1);
887 | Interlocked.Increment(ref _stats.OutgoingMessagesDropped);
888 | return;
889 | }
890 |
891 | // Don't send if stopping.
892 | if (Volatile.Read(ref _stopping) != 0)
893 | {
894 | return;
895 | }
896 |
897 | // Queue the message and trigger send.
898 | Interlocked.Increment(ref _stats.MessagesSent);
899 | Interlocked.Add(ref _stats.BytesSent, data.Length);
900 |
901 | // Check for counter overflow.
902 | if (Interlocked.Increment(ref _overflowCheckCounter) >= OverflowCheckFrequency)
903 | {
904 | Interlocked.Exchange(ref _overflowCheckCounter, 0);
905 | _stats.ResetIfNearOverflow();
906 | }
907 |
908 | SendPending();
909 | }
910 | catch (Exception e)
911 | {
912 | // Return buffer on exception.
913 | if (buffer1 != null)
914 | {
915 | ReturnBufferToPool(buffer1);
916 | }
917 | SafeInvokeOnError(e);
918 | }
919 | }
920 | catch (Exception e)
921 | {
922 | SafeInvokeOnError(e);
923 | }
924 | finally
925 | {
926 | if (buffer2 != null) ReturnBufferToPool(buffer2);
927 | }
928 | }
929 |
930 | ///
931 | /// Send all pending data from the outgoing channel
932 | ///
933 | public void SendPending()
934 | {
935 | if (_disposed != 0 || !_isConnected || _stopping != 0)
936 | {
937 | return;
938 | }
939 |
940 | if (Interlocked.CompareExchange(ref _sending, 1, 0) == 1)
941 | {
942 | return;
943 | }
944 |
945 | TrySendNext();
946 | }
947 |
948 | ///
949 | /// Try to send the next message from the outgoing channel
950 | ///
951 | private void TrySendNext()
952 | {
953 | if (_disposed != 0 || !_isConnected || _stopping != 0)
954 | {
955 | Interlocked.Exchange(ref _sending, 0);
956 | return;
957 | }
958 |
959 | if (_outgoingChannel?.Reader.TryRead(out var rawMessage) != true)
960 | {
961 | Interlocked.Exchange(ref _sending, 0);
962 | return;
963 | }
964 |
965 | SendInternal(rawMessage);
966 | }
967 |
968 | ///
969 | /// Determines if a socket error indicates a normal connection closure rather than an error
970 | ///
971 | private static bool IsConnectionClosedError(SocketError error)
972 | {
973 | return error == SocketError.ConnectionAborted ||
974 | error == SocketError.ConnectionReset ||
975 | error == SocketError.Disconnecting ||
976 | error == SocketError.Shutdown ||
977 | error == SocketError.NotConnected ||
978 | error == SocketError.TimedOut ||
979 | error == SocketError.NetworkDown ||
980 | error == SocketError.NetworkUnreachable ||
981 | error == SocketError.HostDown ||
982 | error == SocketError.HostUnreachable;
983 | }
984 |
985 | ///
986 | /// Internal method to actually send preprocessed data over the socket
987 | ///
988 | private void SendInternal(RawMessage rawMessage)
989 | {
990 | if (_disposed != 0 || !_isConnected)
991 | {
992 | ReturnBufferToPool(rawMessage.BufferWriter);
993 | Interlocked.Exchange(ref _sending, 0);
994 | return;
995 | }
996 |
997 | var bufferSize = _bufferSize;
998 | if (bufferSize == 0)
999 | {
1000 | ReturnBufferToPool(rawMessage.BufferWriter);
1001 | Interlocked.Exchange(ref _sending, 0);
1002 | return;
1003 | }
1004 |
1005 | try
1006 | {
1007 | var writtenMemory = rawMessage.BufferWriter.WrittenMemory.Slice(0, rawMessage.Length);
1008 |
1009 | _sendArg.SetBuffer(MemoryMarshal.AsMemory(writtenMemory));
1010 |
1011 | // Store buffer writer for cleanup after send.
1012 | var previousBufferWriter = Interlocked.Exchange(ref _currentBufferWriter, rawMessage.BufferWriter);
1013 | // Clean up previous writer to prevent leaks.
1014 | if (previousBufferWriter != null)
1015 | {
1016 | ReturnBufferToPool(previousBufferWriter);
1017 | }
1018 |
1019 | if (!_socket.SendAsync(_sendArg))
1020 | {
1021 | HandleSendComplete(_sendArg);
1022 | }
1023 | }
1024 | catch (SocketException se)
1025 | {
1026 | ReturnBufferToPool(rawMessage.BufferWriter);
1027 |
1028 | Interlocked.Exchange(ref _sending, 0);
1029 |
1030 | // Don't report normal connection closure as an error.
1031 | if (!IsConnectionClosedError(se.SocketErrorCode))
1032 | {
1033 | SafeInvokeOnError(se);
1034 | }
1035 |
1036 | StopFast((int)se.SocketErrorCode, "HandleReadWrite SocketException");
1037 | }
1038 | catch (Exception e)
1039 | {
1040 | ReturnBufferToPool(rawMessage.BufferWriter);
1041 |
1042 | Interlocked.Exchange(ref _sending, 0);
1043 | SafeInvokeOnError(e);
1044 | // Stop with callstack for unexpected exceptions.
1045 | Stop("HandleSendComplete Exception", true);
1046 | }
1047 | }
1048 |
1049 | private static void HandleReadWrite(object sender, SocketAsyncEventArgs args)
1050 | {
1051 | NetClient client = args.UserToken as NetClient;
1052 | try
1053 | {
1054 | if (client is not { IsConnected: true })
1055 | return;
1056 |
1057 | switch (args.LastOperation)
1058 | {
1059 | case SocketAsyncOperation.Send:
1060 | client.HandleSendComplete(args);
1061 | break;
1062 | case SocketAsyncOperation.Receive:
1063 | Receive(args);
1064 | break;
1065 | default:
1066 | throw new InvalidOperationException($"Unknown operation: {args.LastOperation}");
1067 | }
1068 | }
1069 | catch (ObjectDisposedException)
1070 | {
1071 | // Ignore if socket is closed.
1072 | }
1073 | catch (SocketException se)
1074 | {
1075 | // Only invoke OnError if it's not a normal connection closure
1076 | if (!IsConnectionClosedError(se.SocketErrorCode))
1077 | {
1078 | client?.SafeInvokeOnError(se);
1079 | }
1080 |
1081 | client?.StopFast((int)se.SocketErrorCode, "HandleReadWrite SocketException");
1082 | }
1083 | catch (Exception e)
1084 | {
1085 | client?.SafeInvokeOnError(e);
1086 | client?.Stop("HandleReadWrite Exception", true); // Include call stack for unexpected exceptions
1087 | }
1088 | }
1089 |
1090 | private void HandleSendComplete(SocketAsyncEventArgs args)
1091 | {
1092 | // Get and clear the current buffer writer.
1093 | ArrayBufferWriter bufferWriterToReturn = Interlocked.Exchange(ref _currentBufferWriter, null);
1094 |
1095 | try
1096 | {
1097 | args.SetBuffer(null, 0, 0);
1098 |
1099 | if (args.SocketError != SocketError.Success)
1100 | {
1101 | Interlocked.Exchange(ref _sending, 0);
1102 |
1103 | // Only invoke OnError if it's not a normal connection closure
1104 | if (!IsConnectionClosedError(args.SocketError))
1105 | {
1106 | SafeInvokeOnError(new SocketException((int)args.SocketError));
1107 | }
1108 |
1109 | StopFast((int)args.SocketError, "HandleSendComplete SocketError");
1110 | return;
1111 | }
1112 |
1113 | // Try to send the next message.
1114 | TrySendNext();
1115 | }
1116 | catch (Exception e)
1117 | {
1118 | Interlocked.Exchange(ref _sending, 0);
1119 | SafeInvokeOnError(e);
1120 | }
1121 | finally
1122 | {
1123 | // Ensure the buffer is always returned to the pool.
1124 | ReturnBufferToPool(bufferWriterToReturn);
1125 | }
1126 | }
1127 |
1128 | private static void Receive(SocketAsyncEventArgs args)
1129 | {
1130 | NetClient client = (NetClient)args.UserToken!;
1131 |
1132 | if (args is { BytesTransferred: > 0, SocketError: SocketError.Success })
1133 | {
1134 | try
1135 | {
1136 | if (client._receiveBuffer == null)
1137 | {
1138 | client.SafeInvokeOnError(CachedReceiveBufferNullException);
1139 | client.Stop("Receive buffer is null");
1140 | return;
1141 | }
1142 |
1143 | client._receiveBuffer.AdvanceWrite(args.BytesTransferred);
1144 | client.ProcessReceivedData();
1145 | }
1146 | catch (Exception e)
1147 | {
1148 | client.SafeInvokeOnError(e);
1149 | }
1150 |
1151 | if (!client._isConnected || client._socket == null || client._receiveBuffer == null)
1152 | {
1153 | return;
1154 | }
1155 |
1156 | client.TryStartReceive(args);
1157 | }
1158 | else
1159 | {
1160 | if (client._isZeroByteReceive && args.SocketError == SocketError.Success)
1161 | {
1162 | client.TryStartReceive(args);
1163 | return;
1164 | }
1165 |
1166 | if (args.SocketError != SocketError.Success)
1167 | {
1168 | client.StopFast((int)args.SocketError, "Socket error");
1169 | }
1170 | else if (args.BytesTransferred == 0)
1171 | {
1172 | // 0 bytes means remote graceful close.
1173 | var additionalInfo = "";
1174 | try
1175 | {
1176 | // Check socket state for more info.
1177 | if (client._socket != null)
1178 | {
1179 | var socketConnected = client._socket.Connected;
1180 | var socketAvailable = client._socket.Available;
1181 | additionalInfo = $" (Socket.Connected={socketConnected}, Available={socketAvailable})";
1182 |
1183 | if (socketConnected && socketAvailable == 0)
1184 | {
1185 | additionalInfo += " - Appears to be graceful close";
1186 | }
1187 | else if (!socketConnected)
1188 | {
1189 | additionalInfo += " - Socket already marked as disconnected";
1190 | }
1191 | }
1192 | }
1193 | catch (Exception ex)
1194 | {
1195 | additionalInfo = $" (Socket check failed: {ex.Message})";
1196 | }
1197 |
1198 | client.Stop($"Remote endpoint closed connection (0 bytes received){additionalInfo}");
1199 | }
1200 | else
1201 | {
1202 | client.Stop($"Unexpected disconnect (BytesTransferred: {args.BytesTransferred})");
1203 | }
1204 | }
1205 | }
1206 |
1207 | ///
1208 | /// Check for complete messages in the receive buffer and directly invoke callbacks
1209 | ///
1210 | private void ProcessReceivedData()
1211 | {
1212 | if (_disposed != 0 || _receiveBuffer == null)
1213 | return;
1214 |
1215 | if (_receiveBuffer.Length == 0)
1216 | {
1217 | return;
1218 | }
1219 |
1220 | var bufferSize = _bufferSize;
1221 | if (bufferSize == 0)
1222 | {
1223 | return;
1224 | }
1225 |
1226 | var data = _receiveBuffer.GetReadSegments();
1227 |
1228 | byte[] temp = null;
1229 | ReadOnlyMemory src = data.First;
1230 |
1231 | if (!data.Second.IsEmpty)
1232 | {
1233 | // Segmented data, use a temporary buffer.
1234 | var totalLength = data.Length;
1235 | temp = ArrayPool.Shared.Rent(totalLength);
1236 | Interlocked.Increment(ref _stats.TempBufferAllocations);
1237 | data.CopyTo(temp);
1238 | src = new ReadOnlyMemory(temp, 0, totalLength);
1239 | }
1240 |
1241 | try
1242 | {
1243 | int totalConsumedFromBuffer = 0;
1244 |
1245 | while (!src.IsEmpty)
1246 | {
1247 | ReadOnlyMemory receivedData = src;
1248 | int consumedFromOrigin = src.Length;
1249 |
1250 | ArrayBufferWriter buffer1 = null;
1251 | if (_middlewares.Count > 0)
1252 | {
1253 | consumedFromOrigin = 0;
1254 |
1255 | buffer1 = GetBufferFromPool();
1256 | var buffer2 = GetBufferFromPool();
1257 | ResetBuffer(buffer1);
1258 | ResetBuffer(buffer2);
1259 | var span = buffer1.GetSpan(src.Length);
1260 | src.Span.CopyTo(span);
1261 | buffer1.Advance(src.Length);
1262 |
1263 | var middlewareSrc = buffer1;
1264 | var middlewareDst = buffer2;
1265 |
1266 | // Process through middleware (reverse order to undo send transformations).
1267 | for (int i = _middlewares.Count - 1; i >= 0; i--)
1268 | {
1269 | try
1270 | {
1271 | var (halt, consumed) = _middlewares[i].ProcessReceive(middlewareSrc.WrittenMemory, middlewareDst);
1272 | // Accumulate consumption from middleware.
1273 | consumedFromOrigin += consumed;
1274 |
1275 | if (halt)
1276 | {
1277 | // Halt processing and advance buffer.
1278 | totalConsumedFromBuffer += consumedFromOrigin;
1279 | _receiveBuffer.AdvanceRead(totalConsumedFromBuffer);
1280 | ReturnBufferToPool(buffer1);
1281 | ReturnBufferToPool(buffer2);
1282 | return;
1283 | }
1284 |
1285 | // Swap buffers for next middleware.
1286 | (middlewareDst, middlewareSrc) = (middlewareSrc, middlewareDst);
1287 |
1288 | // Clear the destination buffer for next use.
1289 | ResetBuffer(middlewareDst);
1290 | }
1291 | catch (Exception e)
1292 | {
1293 | SafeInvokeOnError(e);
1294 | // Halt and advance buffer on error.
1295 | totalConsumedFromBuffer += consumedFromOrigin;
1296 | _receiveBuffer.AdvanceRead(totalConsumedFromBuffer);
1297 | ReturnBufferToPool(buffer1);
1298 | ReturnBufferToPool(buffer2);
1299 | return;
1300 | }
1301 | }
1302 |
1303 | // `middlewareSrc` has the final data.
1304 | receivedData = middlewareSrc.WrittenMemory;
1305 | buffer1 = middlewareSrc;
1306 | ReturnBufferToPool(middlewareDst);
1307 | }
1308 |
1309 | // No more data, advance buffer and break.
1310 | if (receivedData.IsEmpty)
1311 | {
1312 | totalConsumedFromBuffer += consumedFromOrigin;
1313 | if (buffer1 != null)
1314 | {
1315 | ReturnBufferToPool(buffer1);
1316 | }
1317 | break;
1318 | }
1319 |
1320 | // Invoke callback with processed data.
1321 | try
1322 | {
1323 | OnDataReceived?.Invoke(receivedData);
1324 | }
1325 | catch (Exception callbackEx)
1326 | {
1327 | // Log callback errors, but continue.
1328 | SafeInvokeOnError(callbackEx);
1329 | }
1330 | finally
1331 | {
1332 | if (buffer1 != null)
1333 | {
1334 | ReturnBufferToPool(buffer1);
1335 | }
1336 |
1337 | // Track stats.
1338 | _localMessagesReceived++;
1339 | _localBytesReceived += consumedFromOrigin;
1340 |
1341 | // Flush batched stats.
1342 | if (_localMessagesReceived >= DefaultConfig.BatchSize)
1343 | {
1344 | Interlocked.Add(ref _stats.MessagesReceived, _localMessagesReceived);
1345 | Interlocked.Add(ref _stats.BytesReceived, _localBytesReceived);
1346 | _localMessagesReceived = 0;
1347 | _localBytesReceived = 0;
1348 | }
1349 | }
1350 |
1351 | totalConsumedFromBuffer += consumedFromOrigin;
1352 |
1353 | if (consumedFromOrigin >= src.Length)
1354 | {
1355 | break;
1356 | }
1357 | src = src.Slice(consumedFromOrigin);
1358 |
1359 | // Use SpinWait instead of Thread.Yield for better performance.
1360 | if (++_yieldCounter >= DefaultConfig.YieldThreshold)
1361 | {
1362 | _yieldCounter = 0;
1363 | Thread.Yield();
1364 | }
1365 |
1366 | // Check for counter overflow.
1367 | if (Interlocked.Increment(ref _overflowCheckCounter) >= OverflowCheckFrequency)
1368 | {
1369 | Interlocked.Exchange(ref _overflowCheckCounter, 0);
1370 | _stats.ResetIfNearOverflow();
1371 | }
1372 | }
1373 |
1374 | // Advance read position.
1375 | if (totalConsumedFromBuffer > 0)
1376 | {
1377 | _receiveBuffer.AdvanceRead(totalConsumedFromBuffer);
1378 | }
1379 | }
1380 | finally
1381 | {
1382 | // Return temp buffer to pool.
1383 | if (temp != null)
1384 | {
1385 | ArrayPool.Shared.Return(temp);
1386 | }
1387 |
1388 | // Flush remaining batched stats.
1389 | if (_localMessagesReceived > 0)
1390 | {
1391 | Interlocked.Add(ref _stats.MessagesReceived, _localMessagesReceived);
1392 | Interlocked.Add(ref _stats.BytesReceived, _localBytesReceived);
1393 | _localMessagesReceived = 0;
1394 | _localBytesReceived = 0;
1395 | }
1396 | }
1397 | }
1398 |
1399 | ///
1400 | /// Get detailed status information for debugging data loss issues
1401 | ///
1402 | public string GetDiagnosticInfo()
1403 | {
1404 | if (_disposed != 0)
1405 | return "NetClient disposed";
1406 |
1407 | // Cache values for performance.
1408 | var stats = _stats;
1409 | var isConnected = IsConnected;
1410 | var hasPendingSends = HasPendingSends;
1411 | var bufferLength = _receiveBuffer?.Length ?? 0;
1412 | var bufferFreeSpace = _receiveBuffer?.FreeSpace ?? 0;
1413 | var bufferCapacity = _receiveBuffer?.Capacity ?? 0;
1414 | var isZeroByteReceive = _isZeroByteReceive;
1415 | var isSending = _sending != 0;
1416 |
1417 | _diagnosticInfoBuilder.Clear();
1418 |
1419 | _diagnosticInfoBuilder
1420 | .Append("NetClient ").Append(Id).AppendLine(" Diagnostics:")
1421 | .Append(" Connected: ").AppendLine(isConnected ? "True" : "False")
1422 | .Append(" Messages Received: ").AppendLine(stats.MessagesReceived.ToString())
1423 | .Append(" Outgoing Messages Dropped: ").Append(stats.OutgoingMessagesDropped)
1424 | .Append(" (Drop Rate: ").Append((stats.OutgoingDropRate * 100).ToString("F2")).AppendLine("%)")
1425 | .Append(" Messages Sent: ").AppendLine(stats.MessagesSent.ToString())
1426 | .Append(" Errors Skipped: ").AppendLine(stats.ErrorsSkipped.ToString())
1427 | .Append(" Temp Buffer Allocs: ").AppendLine(stats.TempBufferAllocations.ToString())
1428 | .Append(" Bytes Received: ").AppendLine(stats.BytesReceived.ToString())
1429 | .Append(" Bytes Sent: ").AppendLine(stats.BytesSent.ToString())
1430 | .Append(" Outgoing Channel Pending: ").AppendLine(hasPendingSends ? "Yes" : "No")
1431 | .Append(" Receive Buffer: ").Append(bufferLength).Append('/').Append(bufferCapacity)
1432 | .Append(" bytes (Free: ").Append(bufferFreeSpace).AppendLine(")")
1433 | .Append(" Zero-Byte Receive Mode: ").AppendLine(isZeroByteReceive ? "True" : "False")
1434 | .Append(" Sending: ").AppendLine(isSending ? "True" : "False");
1435 |
1436 | return _diagnosticInfoBuilder.ToString();
1437 | }
1438 |
1439 | ///
1440 | /// Dispose of resources
1441 | ///
1442 | public void Dispose()
1443 | {
1444 | // Ensure single-threaded disposal.
1445 | if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 1)
1446 | {
1447 | return;
1448 | }
1449 |
1450 | Stop();
1451 |
1452 | GC.SuppressFinalize(this);
1453 | }
1454 |
1455 | ///
1456 | /// Finalizer
1457 | ///
1458 | ~NetClient()
1459 | {
1460 | Dispose();
1461 | }
1462 | }
1463 | }
1464 |
--------------------------------------------------------------------------------