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