diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..bb05f92 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,128 @@ +name: Build and Publish + +on: + push: + branches: [ master, 2.X ] + tags: + - '*' + pull_request: + branches: [ master, 2.X ] + workflow_dispatch: + +env: + CHANGELOG_PATH: ./Changelog.md + CODE_COVERAGE_PATH: ./Coverage.xml + SOLUTION_PATH: ./Source/Reloaded.Messaging.sln + NUPKG_GLOB: ./Source/**/*.nupkg + IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }} + RELEASE_TAG: ${{ github.ref_name }} + +jobs: + build: + runs-on: windows-2022 + defaults: + run: + shell: pwsh + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup .NET Core SDK (3.1.x) + uses: actions/setup-dotnet@v1.9.0 + with: + # Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x + dotnet-version: 3.1.x + + - name: Setup .NET Core SDK (5.0.x) + uses: actions/setup-dotnet@v1.9.0 + with: + # Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x + dotnet-version: 5.0.x + + - name: Setup .NET Core SDK (6.0.x) + uses: actions/setup-dotnet@v1.9.0 + with: + # Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x + dotnet-version: 6.0.x + + # Required for C#10 features. + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Setup AutoChangelog + run: npm install -g auto-changelog + + - name: Get Dotnet Info + run: dotnet --info + + - name: Build + run: dotnet build -c Release "$env:SOLUTION_PATH" + + - name: Test + run: | + dotnet test ./Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput="../../$env:CODE_COVERAGE_PATH" --% /p:Exclude=\"[xunit.*]*\" + + - name: Codecov + # You may pin to the exact commit or the version. + # uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b + uses: codecov/codecov-action@v2.1.0 + with: + # Comma-separated list of files to upload + files: ${{ env.CODE_COVERAGE_PATH }} + + - name: Create Changelog (on Tag) + run: | + if ($env:IS_RELEASE -eq 'true') + { + auto-changelog --sort-commits date --hide-credit --template keepachangelog --commit-limit false --unreleased --starting-version "$env:RELEASE_TAG" --output "$env:CHANGELOG_PATH" + } + else + { + auto-changelog --sort-commits date --hide-credit --template keepachangelog --commit-limit false --unreleased --output "$env:CHANGELOG_PATH" + } + + - name: Upload NuGet Package Artifact + uses: actions/upload-artifact@v2.2.4 + with: + # Artifact name + name: NuGet Packages + # A file, directory or wildcard pattern that describes what to upload + path: | + ${{ env.NUPKG_GLOB }} + + - name: Upload Changelog Artifact + uses: actions/upload-artifact@v2.2.4 + with: + # Artifact name + name: Changelog + # A file, directory or wildcard pattern that describes what to upload + path: ${{ env.CHANGELOG_PATH }} + retention-days: 0 + + + - name: Upload to GitHub Releases + uses: softprops/action-gh-release@v0.1.14 + if: env.IS_RELEASE == 'true' + with: + # Path to load note-worthy description of changes in release from + body_path: ${{ env.CHANGELOG_PATH }} + # Newline-delimited list of path globs for asset files to upload + files: | + ${{ env.NUPKG_GLOB }} + ${{ env.CHANGELOG_PATH }} + + - name: Upload to NuGet (on Tag) + env: + NUGET_KEY: ${{ secrets.NUGET_KEY }} + if: env.IS_RELEASE == 'true' + run: | + $items = Get-ChildItem -Path "$env:NUPKG_GLOB" -Recurse + Foreach ($item in $items) + { + Write-Host "Pushing $item" + dotnet nuget push "$item" -k "$env:NUGET_KEY" -s "https://api.nuget.org/v3/index.json" --skip-duplicate + } \ No newline at end of file diff --git a/.github/workflows/deploy-mkdocs.yml b/.github/workflows/deploy-mkdocs.yml new file mode 100644 index 0000000..7a2567a --- /dev/null +++ b/.github/workflows/deploy-mkdocs.yml @@ -0,0 +1,32 @@ +name: DeployMkDocs + +# Controls when the action will run. +on: + # Triggers the workflow on push on the master branch + push: + branches: [ master, 2.X ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout Branch + uses: actions/checkout@v2 + + # Deploy MkDocs + - name: Deploy MkDocs + # You may pin to the exact commit or the version. + # uses: mhausenblas/mkdocs-deploy-gh-pages@66340182cb2a1a63f8a3783e3e2146b7d151a0bb + uses: mhausenblas/mkdocs-deploy-gh-pages@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/Docs/ImplementingCompressorsSerializers.md b/Docs/ImplementingCompressorsSerializers.md deleted file mode 100644 index f1a2e76..0000000 --- a/Docs/ImplementingCompressorsSerializers.md +++ /dev/null @@ -1,81 +0,0 @@ -## Implementing Compressors & Serializers -Reloaded.Messaging allows you to specify your own serializers and compressors responsible for the serialization of the message to be transmitted. - -Adding support for these is very trivial. - -### Serializers - -To implement a serializer, simply make a class that implements the `ISerializer` interface. - -```csharp -public interface ISerializer -{ - TStruct Deserialize(byte[] serialized); - byte[] Serialize(ref TStruct item); -} -``` - -A simple example implementation using [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp) could look like this: - -```csharp -public class MsgPackSerializer : ISerializer -{ - public TStruct Deserialize(byte[] serialized) => MessagePackSerializer.Deserialize(serialized); - - public byte[] Serialize(ref TStruct item) => MessagePackSerializer.Serialize(item); -} -``` - -There is no default serializer, however one must be specified. -All serializers can be found in the `Reloaded.Messaging.Serializer` namespace, with serializers available as their own NuGet packages. - -### Compressors - -Implementing Compressors is virtually identical to implementing a Serializer. - -```csharp -public interface ICompressor -{ - byte[] Compress(byte[] data); - byte[] Decompress(byte[] data); -} -``` - -A simple example using [ZstdNet](https://github.com/skbkontur/ZstdNet) could look like this: - -```csharp -public class ZStandardCompressor : ICompressor, IDisposable -{ - public readonly ZstdNet.Compressor Compressor; - public readonly Decompressor Decompressor; - - public ZStandardCompressor(CompressionOptions compressionOptions = null, DecompressionOptions decompressionOptions = null) - { - Compressor = compressionOptions != null ? new ZstdNet.Compressor(compressionOptions) : new ZstdNet.Compressor(); - - Decompressor = decompressionOptions != null ? new Decompressor(decompressionOptions) : new Decompressor(); - } - - // Disposal - ~ZStandardCompressor() - { - Dispose(); - } - - public void Dispose() - { - Compressor?.Dispose(); - Decompressor?.Dispose(); - GC.SuppressFinalize(this); - } - - // ICompressor - public byte[] Compress(byte[] data) => Compressor.Wrap(data); - public byte[] Decompress(byte[] data) => Decompressor.Unwrap(data); -} -``` - -All compressors can be found in the `Reloaded.Messaging.Compressor` namespace, with compressors available as separate NuGet packages. - -If `null` is specified for the compressor, no compression will be performed. - diff --git a/Docs/UseAsNetworkingLibrary.md b/Docs/UseAsNetworkingLibrary.md deleted file mode 100644 index 62c1013..0000000 --- a/Docs/UseAsNetworkingLibrary.md +++ /dev/null @@ -1,136 +0,0 @@ -# Table of Contents -- [Usage (As Networking Library)](#usage-as-networking-library) - - [How It Works (Summary)](#how-it-works-summary) - - [A: Unique Message Identifier](#a-unique-message-identifier) - - [B: Implementing IMessage\](#b-implementing-imessagemessagetype) - - [C. Connecting Host & Client](#c-connecting-host--client) - - [D. Receiving Messages](#d-receiving-messages) - - [E. Send Messages](#e-send-messages) -- [Custom Compressors & Serializers](#custom-compressors--serializers) -- [Other Information](#other-information) - - [Overriding Serializers & Compressors at Runtime](#overriding-serializers--compressors-at-runtime) - - [Default Settings (LiteNetLib)](#default-settings-litenetlib) - - [Connection Requests](#connection-requests) - - [Packet Handling](#packet-handling) - -## Usage (As Networking Library) - -### How It Works (Summary) -A. User specifies or chooses an individual unmanaged type `TMessageType` (recommend enum), where each unique value corresponds to different message structure. - -B. User implements interface `IMessage` in types they want to send over the network. - -C. User creates `SimpleHost` instance(s) for Server/Client with type from `A` as generic type. - -D. User registers methods to handle different values for `TMessageType`. - -And then message sending/receiving can proceed. - -Note: A complete working example can be found in the basic test collection, `Reloaded.Messaging.Tests`. - -### A: Unique Message Identifier -A unique message identifier `TMessageType` can be any unmanaged type. -The recommended type is enum. - -```csharp -// We have less than 256 values, so use byte. -// Default for enum is int, but we don't need extra 3 bytes of overhead in every message. -public enum MessageType : byte -{ - String, - Vector3 -} -``` - -### B: Implementing IMessage\ -The interface specifies the compressor and serializer used to pack the specified structure. -It acts as a contract and therefore should match between the server and client. - -No information about the serializer or compressor is sent with any of the packets as that would incur unnecessary additional overhead. - -```csharp -public struct Vector3 : IMessage -{ - // IMessage - public MessageType GetMessageType() => MessageType.Vector3; - public ISerializer GetSerializer() => new MsgPackSerializer(true); - public ICompressor GetCompressor() => null; - - // Members - public float X { get; set; } - public float Y { get; set; } - public float Z { get; set; } -} -``` - -A serializer must be specified. Compressor is optional. - -### C. Connecting Host & Client - -The following example creates a new server and client on the local machine and connects them to each other. - -```csharp -// DefaultPassword = "RandomString" -// MessageType = enum (byte) (see above) -SimpleServer = new SimpleHost(true, DefaultPassword); -SimpleClient = new SimpleHost(false, DefaultPassword); - -SimpleServer.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); -SimpleClient.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); -SimpleClient.NetManager.Connect(new IPEndPoint(IPAddress.Loopback, SimpleServer.NetManager.LocalPort), DefaultPassword); -``` - -### D. Receiving Messages -```csharp -// Register a function "Handler" to deal with incoming messages of type Vector3. (MessageType is obtained from IMessage Interface) -SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); - -static void Handler(ref NetMessage netMessage) -{ - var vector3 = netMessage.Message; - // Do something with Vector3 -} -``` - -### E. Send Messages -To send a message, create an instance of `Message`, where `TStruct` is a struct from `B` that inherits `IMessage`. - -```csharp -var vectorMessage = new Message(message); // Wraps the message for sending. -byte[] data = vectorMessage.Serialize(); // Serializes and compresses using Serializer/Compressor defined in IMessage implementation. - -SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); // Regular LiteNetLib usage. -``` - -## Custom Compressors & Serializers -See [ImplementingCompressorsSerializers.md](./ImplementingCompressorsSerializers.md) - -## Other Information - -### Overriding Serializers & Compressors at Runtime -*Note: This feature is mainly intended for benchmarking and testing.* -*Changes are client-side (program-side) only and not broadcasted to clients etc.* - -It is possible to override the compressor and/or serializer used to handle a specific type at runtime. - -Usage Examples: -```csharp -// Override Vector3 -Overrides.SerializerOverride[typeof(Vector3)] = new MsgPackSerializer(false); -Overrides.CompressorOverride[typeof(Vector3)] = null; - -// Remove overrides for Vector3 -Overrides.SerializerOverride.Remove(typeof(Vector3)); -Overrides.CompressorOverride.Remove(typeof(Vector3)); -``` - -### Default Settings (LiteNetLib) -`SimpleHost` uses the following default settings for the LiteNetLib library. - -#### Connection Requests -- Subscribes to `ConnectionRequestEvent`, allowing clients to connect only with password set in constructor. (Which can be changed after instantiation) - -#### Packet Handling -- Sets `UnsyncedEvents` to true. Messages are received automatically on background thread. -- Sets `AutoRecycle` to true. Automatically recycling NetPacketReader. -- Subscribes to `NetworkReceiveEvent`, to automatically handle incoming packets that have been assigned to the `MessageHandler`. \ No newline at end of file diff --git a/Docs/UseAsSerializationLibrary.md b/Docs/UseAsSerializationLibrary.md deleted file mode 100644 index 5144c54..0000000 --- a/Docs/UseAsSerializationLibrary.md +++ /dev/null @@ -1,47 +0,0 @@ -## Usage (As Serialization Library) - -### Define Serializable Struct/Class - -In order to enable serialization for your class, you should implement the `ISerializable` interface. - -```csharp -public struct Vector3 : ISerializable -{ - // ISerializable - public ISerializer GetSerializer() => ReloadedMemorySerializer(false); - public ICompressor GetCompressor() => null; - - // Members - public float X { get; set; } - public float Y { get; set; } - public float Z { get; set; } -} -``` - -Returning an `ISerializer` is required. Returning an `ICompressor` is optional. - -### Serialize - -To serialize an instance, call the extension method `Serializable.Serialize()` in `Reloaded.Messaging.Interfaces.Serializable`. - -```csharp -var vector = new Vector3(0, 25, 100); -byte[] data = vector.Serialize(); -``` - -### Deserialize - -To serialize an instance, call the extension method `Serializable.Deserialize()` in `Reloaded.Messaging.Interfaces.Serializable`. - -```csharp -byte[] data = vector.Serialize(); -var newVector = Serializable.Deserialize(data); -``` - -### Custom Compressors & Serializers -See [ImplementingCompressorsSerializers.md](./ImplementingCompressorsSerializers.md) - -### Other Notes - -- It is possible to use serializer specific attributes/markup etc. for your struct members. -- Some serializers may require the use of properties for serialization/deserialization. \ No newline at end of file diff --git a/README.md b/README.md index 92a90d0..56241f9 100644 --- a/README.md +++ b/README.md @@ -6,42 +6,28 @@

-# Packages -**Reloaded.Messaging:** NuGet - -**Reloaded.Messaging.Interfaces:** NuGet - -**Reloaded.Messaging.Serializer.MessagePack**: NuGet - -**Reloaded.Messaging.Serializer.ReloadedMemory**: NuGet - -**Reloaded.Messaging.Serializer.SystemTextJson**: NuGet - -**Reloaded.Messaging.Serializer.NewtonsoftJson**: NuGet - -**Reloaded.Messaging.Compressor.ZStandard**: NuGet - # Introduction -Reloaded.Networking is [Reloaded II](https://github.com/Reloaded-Project/Reloaded-II/)'s Networking and Serialization library. The main goal for the library is to provide an extensible "event-like" solution for passing messages across a local or remote network that extends on the base functionality of [LiteNetLib](https://github.com/RevenantX/LiteNetLib) by Ruslan Pyrch (RevenantX) . - -It has been slightly extended in the hope of becoming more general purpose, perhaps to be reused in other projects. -## Idea -`Reloaded.Networking` is a simple barebones library to solve a deceptively annoying problem: Writing code that distinguishes the type of message received over a network and performs a specific action. +Reloaded.Networking is library that adds support for simple, high performance message packing to existing networking libraries. -## Characteristics -- Minimal networking overhead in most use cases (1 byte)*. -- Choice of serializer/compressor on a per type (struct/class) basis. -- Simple to use. +Specifically, it provides a minimal framework for performing the following tasks: -*Assuming user has less than 256 unique types of network messages. +- Asynchronous message processing for external networking libraries. +- Sending/Receiving messages with (de)serialization and [optional] (de)compression. +- Automatically dispatching messages to appropriate handlers (per message type). -*Alternative unmanaged types (e.g. short, int) can be specified increasing overhead to `sizeof(type)` and respectively increasing max unique types.* +It was originally created for Reloaded II, however has been extended in the hope of becoming a more general purpose library. + +This library is heavily optimized for achieving high throughput for messages `< 128KB`. -## Usage - -[Usage: As Networking Library](./Docs/UseAsNetworkingLibrary.md) +## Characteristics +- High performance. (Memory pooling, allocation free). +- Low networking overhead. +- Custom serializer/compressor per class type. +- Simple message packing/protocol. +- 1 byte overhead for uncompressed, 5 bytes for compressed. +- Unsafe. -[Usage: As Serialization Library](./Docs/UseAsSerializationLibrary.md) +## Documentation -[Adding 3rd Party Compressors & Serializers](./Docs/ImplementingCompressorsSerializers.md) +More information can be found in the [dedicated documentation site](https://reloaded-project.github.io/Reloaded.Messaging). \ No newline at end of file diff --git a/Source/NuGet-Icon.png b/Source/NuGet-Icon.png new file mode 100644 index 0000000..13e84a3 Binary files /dev/null and b/Source/NuGet-Icon.png differ diff --git a/Source/Reloaded.Messaging.Benchmarks/DeserializationBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/DeserializationBenchmark.cs new file mode 100644 index 0000000..6436109 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/DeserializationBenchmark.cs @@ -0,0 +1,97 @@ +using System.Runtime; +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; +using FastSerialization; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Serializer.MessagePack; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class DeserializationBenchmark +{ + public const int UnrollFactor = 5; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + private RecyclableMemoryStreamManager _streamManager = new(); + private RecyclableMemoryStream _stream; + private byte[][] _jsons = null!; + private byte[][] _msgPacks = null!; + + private SourceGeneratedSystemTextJsonSerializer _srcGenSystemTextJsonSerializer; + private SystemTextJsonSerializer _systemTextJsonSerializer = new(); + private MessagePackSerializer _messagePackSerializer = new(); + + [GlobalSetup] + public void Setup() + { + _srcGenSystemTextJsonSerializer = new(ModConfigContext.Default.ModConfig); + + // Prepare the data by serializing first. + using (var serializeStream = (RecyclableMemoryStream)_streamManager.GetStream()) + { + var items = ModConfig.Create(NumItems); + _jsons = new byte[NumItems][]; + _msgPacks = new byte[NumItems][]; + + for (int x = 0; x < items.Length; x++) + { +#pragma warning disable CS0618 + serializeStream.SetLength(0); + _srcGenSystemTextJsonSerializer.Serialize(ref items.GetWithoutBoundsChecks(x), serializeStream); + _jsons[x] = serializeStream.ToArray(); + + serializeStream.SetLength(0); + _messagePackSerializer.Serialize(ref items.GetWithoutBoundsChecks(x), serializeStream); + _msgPacks[x] = serializeStream.ToArray(); +#pragma warning restore CS0618 + } + } + + _stream = (RecyclableMemoryStream)_streamManager.GetStream(); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _jsons = null!; + _msgPacks = null!; + GC.Collect(); + } + + [IterationCleanup] + public void IterationSetup() + { + _stream.Seek(0, SeekOrigin.Begin); + } + + [Benchmark] + public void SystemTextJson() => BenchmarkCommon(_jsons, _systemTextJsonSerializer); + + [Benchmark] + public void SystemTextJsonSrcGen() => BenchmarkCommon(_jsons, _srcGenSystemTextJsonSerializer); + + [Benchmark] + public void MessagePack() => BenchmarkCommon(_msgPacks, _messagePackSerializer); + + private void BenchmarkCommon(byte[][] items, TSerializer serializer) where TSerializer : ISerializer + { + var numIterations = _jsons.Length / UnrollFactor; + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 0).AsSpanFast()); + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 1).AsSpanFast()); + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 2).AsSpanFast()); + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 3).AsSpanFast()); + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 4).AsSpanFast()); + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/MessageHandlerBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/MessageHandlerBenchmark.cs new file mode 100644 index 0000000..bc28613 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/MessageHandlerBenchmark.cs @@ -0,0 +1,79 @@ +using BenchmarkDotNet.Attributes; +using FastSerialization; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces.Utilities; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class MessageHandlerBenchmark +{ + private MessageDispatcher _dispatcher; + private byte[][] _jsons = null!; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + private const int UnrollFactor = 10; + + [GlobalSetup] + public void Setup() + { + // Prepare the data by serializing first. + var streamManager = new RecyclableMemoryStreamManager(); + using (var serializeStream = (RecyclableMemoryStream)streamManager.GetStream()) + { + var serializer = new SourceGeneratedSystemTextJsonSerializer(ModConfigMessageContext.Default.ModConfigMessage); + var items = ModConfig.Create(NumItems); + _jsons = new byte[NumItems][]; + + for (int x = 0; x < items.Length; x++) + { +#pragma warning disable CS0618 + serializeStream.SetLength(0); + serializer.Serialize(ref items.GetWithoutBoundsChecks(x), serializeStream); + _jsons[x] = serializeStream.ToArray(); +#pragma warning restore CS0618 + } + } + + _dispatcher = new MessageDispatcher(); + _dispatcher.AddOrOverrideHandler(new DummyMessageHandlerNoDeserialize, NullCompressor>()); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _jsons = null; + GC.Collect(); + } + + + [Benchmark(Baseline = true)] + public void HandleMessage() + { + // Add handler. + var dispatcher = _dispatcher; + var numIterations = _jsons.Length / UnrollFactor; + var extraData = 0; + + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 0).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 1).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 2).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 3).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 4).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 5).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 6).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 7).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 8).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 9).AsSpanFast(), ref extraData); + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/MessagePackingRealScenarioBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/MessagePackingRealScenarioBenchmark.cs new file mode 100644 index 0000000..8881da7 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/MessagePackingRealScenarioBenchmark.cs @@ -0,0 +1,109 @@ +using BenchmarkDotNet.Attributes; +using Reloaded.Messaging.Benchmarks.Structures; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Utilities; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces.Utilities; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class MessagePackingRealScenarioBenchmark +{ + private ModConfigMessage[]? _items; + + private RecyclableMemoryStreamManager _streamManager = new(); + private MessageDispatcher _dispatcher; + private SourceGeneratedSystemTextJsonSerializer _srcGenSystemTextJsonSerializer; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + [GlobalSetup] + public void Setup() + { + _srcGenSystemTextJsonSerializer = new SourceGeneratedSystemTextJsonSerializer(ModConfigMessageContext.Default.ModConfigMessage); + _dispatcher = new MessageDispatcher(); + _items = ModConfig.Create(NumItems); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _items = null; + GC.Collect(); + } + + + [Benchmark(Baseline = true)] + public void SerializeOnly_NoPack_To_SingleBuffer() + { + using var buffer = (RecyclableMemoryStream)_streamManager.GetStream(); + for (int x = 0; x < _items!.Length; x++) + _srcGenSystemTextJsonSerializer.Serialize(ref _items.GetWithoutBoundsChecks(x), buffer); + } + + [Benchmark] + public void SerializeOnly_NoPack_To_BufferPerMessage() + { + for (int x = 0; x < _items!.Length; x++) + { + using var buffer = (RecyclableMemoryStream)_streamManager.GetStream(); + _srcGenSystemTextJsonSerializer.Serialize(ref _items.GetWithoutBoundsChecks(x), buffer); + } + } + + [Benchmark] + public void Serialize_And_Pack() + { + var dummy = new ModConfigMessage(); + for (int x = 0; x < _items!.Length; x++) + { + using var serialized = dummy.Serialize(ref _items.GetWithoutBoundsChecks(x)); + // Calling span might lead to a memory copy operation, hence to make the + // test fair, we need to call it on the baseline too. + var _ = serialized.Span; + } + } + + [Benchmark] + public void Serialize_And_Pack_And_Handle() + { + // Copy should be cheap here, dispatcher is small. + var dispatcher = _dispatcher; + dispatcher.AddOrOverrideHandler(new DummyMessageHandlerNoDeserialize, NullCompressor>()); + var dummyMsg = new ModConfigMessage(); + int dummy = 0; + + for (int x = 0; x < _items!.Length; x++) + { + using var serialized = dummyMsg.Serialize(ref _items.GetWithoutBoundsChecks(x)); + dispatcher.Dispatch(serialized.Span, ref dummy); + } + + dispatcher.RemoveHandler(ModConfigMessage.MessageType); + } + + [Benchmark] + public void Serialize_And_Pack_And_Handle_And_Unpack_And_Deserialize() + { + // Copy should be cheap here, dispatcher is small. + var dispatcher = _dispatcher; + _items[0].AddToDispatcher(new DummyCallback(), ref _dispatcher); + + var dummyMsg = new ModConfigMessage(); + int dummy = 0; + + for (int x = 0; x < _items!.Length; x++) + { + using var serialized = dummyMsg.Serialize(ref _items.GetWithoutBoundsChecks(x)); + dispatcher.Dispatch(serialized.Span, ref dummy); + } + + dispatcher.RemoveHandler(ModConfigMessage.MessageType); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/PackOverheadBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/PackOverheadBenchmark.cs new file mode 100644 index 0000000..3f28d2b --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/PackOverheadBenchmark.cs @@ -0,0 +1,112 @@ +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Messages; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class PackOverheadBenchmark +{ + private ModConfigMessageWithDummySerializer[]? _items; + private ModConfigMessageWithDummyCompressor[]? _itemsCompressor; + + private RecyclableMemoryStream _stream; + private RecyclableMemoryStreamManager _streamManager = new(); + private DummySerializer _dummySerializer; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + private const int UnrollFactor = 10; + + [GlobalSetup] + public void Setup() + { + _stream = (RecyclableMemoryStream)_streamManager.GetStream(); + _dummySerializer = new(); + _items = ModConfig.Create(NumItems); + _itemsCompressor = ModConfig.Create(NumItems); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _items = null; + GC.Collect(); + } + + + [Benchmark(Baseline = true)] + [SkipLocalsInit] + public void DummySerializeOnly() + { + var numIterations = _items!.Length / UnrollFactor; + var serializer = _dummySerializer; + + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 0), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 1), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 2), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 3), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 4), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 5), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 6), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 7), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 8), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 9), _stream); + } + } + + [Benchmark] + [SkipLocalsInit] + public void DummySerialize_And_Pack() + { + var numIterations = _items!.Length / UnrollFactor; + + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 0), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 1), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 2), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 3), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 4), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 5), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 6), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 7), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 8), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 9), _stream).Dispose(); + } + } + + [Benchmark] + [SkipLocalsInit] + public void DummySerialize_And_Pack_Compressed() + { + // This test is OK because the actual value isn't serialized. + var numIterations = _itemsCompressor!.Length / UnrollFactor; + var compressor = new DummyCompressor(); + + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 0), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 1), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 2), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 3), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 4), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 5), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 6), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 7), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 8), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 9), _stream, compressor).Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Program.cs b/Source/Reloaded.Messaging.Benchmarks/Program.cs new file mode 100644 index 0000000..fbf1ead --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Program.cs @@ -0,0 +1,56 @@ +// See https://aka.ms/new-console-template for more information + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using Reloaded.Messaging.Benchmarks; +using Reloaded.Messaging.Benchmarks.Utilities; + +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); + +public class InProcessConfig : ManualConfig +{ + public InProcessConfig(params IColumn[] extraColumns) + { + Add(DefaultConfig.Instance); + foreach (var column in extraColumns) + AddColumn(column); + + AddJob(Job.Default + .WithToolchain(InProcessEmitToolchain.Instance) + .WithId(".NET (Current Process)")); + } +} + +public class OperationsPerSecondColumn : IColumn +{ + public string Id { get; } = nameof(OperationsPerSecondColumn); + public string ColumnName { get; } = "Operations/s"; + public bool AlwaysShow { get; } = true; + public ColumnCategory Category { get; } = ColumnCategory.Custom; + public int PriorityInCategory { get; } + public bool IsNumeric { get; } = true; + public UnitType UnitType { get; } = UnitType.Size; + public string Legend { get; } + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + var ourReport = summary.Reports.First(x => x.BenchmarkCase.Equals(benchmarkCase)); + var mean = ourReport.ResultStatistics.Mean; + var meanSeconds = mean / 1000_000_000F; + + return $"{(Constants.DefaultOperationCount / meanSeconds):#####.00}"; + } + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) => GetValue(summary, benchmarkCase); + + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + public bool IsAvailable(Summary summary) => true; +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj b/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj new file mode 100644 index 0000000..d7bb76a --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj @@ -0,0 +1,27 @@ + + + + Exe + net6.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + diff --git a/Source/Reloaded.Messaging.Benchmarks/SerializationBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/SerializationBenchmark.cs new file mode 100644 index 0000000..0c45a2d --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/SerializationBenchmark.cs @@ -0,0 +1,73 @@ +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Serializer.MessagePack; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class SerializationBenchmark +{ + public const int UnrollFactor = 5; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + private ModConfig[]? _items; + private RecyclableMemoryStreamManager _streamManager = new(); + private RecyclableMemoryStream _stream; + + private SourceGeneratedSystemTextJsonSerializer _srcGenSystemTextJsonSerializer; + private SystemTextJsonSerializer _systemTextJsonSerializer = new(); + private MessagePackSerializer _messagePackSerializer = new(); + + [GlobalSetup] + public void Setup() + { + _srcGenSystemTextJsonSerializer = new(ModConfigContext.Default.ModConfig); + + _items = ModConfig.Create(NumItems); + _stream = (RecyclableMemoryStream)_streamManager.GetStream(); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _items = null; + GC.Collect(); + } + + [IterationCleanup] + public void IterationSetup() + { + _stream.Seek(0, SeekOrigin.Begin); + } + + [Benchmark] + public void SystemTextJson() => BenchmarkCommon(_systemTextJsonSerializer); + + [Benchmark] + public void SystemTextJsonSrcGen() => BenchmarkCommon(_srcGenSystemTextJsonSerializer); + + [Benchmark] + public void MessagePack() => BenchmarkCommon(_messagePackSerializer); + + private void BenchmarkCommon(TSerializer serializer) where TSerializer : ISerializer + { + var numIterations = _items.Length / UnrollFactor; + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 0), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 1), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 2), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 3), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 4), _stream); + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Structures/ModConfig.cs b/Source/Reloaded.Messaging.Benchmarks/Structures/ModConfig.cs new file mode 100644 index 0000000..a291b80 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Structures/ModConfig.cs @@ -0,0 +1,104 @@ +using Bogus; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Serializer.MessagePack; +using System.Text.Json.Serialization; +using Reloaded.Messaging.Benchmarks.Utilities; + +namespace Reloaded.Messaging.Benchmarks.Structures; + +/// +/// Copy of Reloaded-II's ModConfig +/// +public class ModConfig +{ + + /* Class members. */ + public string? ModId { get; set; } + public string? ModName { get; set; } + public string? ModAuthor { get; set; } + public string? ModVersion { get; set; } + public string? ModDescription { get; set; } + public string? ModDll { get; set; } + + public string? ModIcon { get; set; } + public string? ModR2RManagedDll32 { get; set; } + public string? ModR2RManagedDll64 { get; set; } + public string? ModNativeDll32 { get; set; } + public string? ModNativeDll64 { get; set; } + public bool? IsLibrary { get; set; } + public string? ReleaseMetadataFileName { get; set; } + + public Dictionary? PluginData { get; set; } + + public bool? IsUniversalMod { get; set; } + + public string[]? ModDependencies { get; set; } + public string[]? OptionalDependencies { get; set; } + public string[]? SupportedAppId { get; set; } + + public static T[] Create(int numConfigs) where T : ModConfig, new() + { + var dependencies = new Faker().CustomInstantiator(f => f.Hacker.Random.Hash()).GenerateArray(numConfigs); + var applications = new Faker().CustomInstantiator(f => f.System.FileName(".exe")).GenerateArray(Math.Max(numConfigs / 100, 1)); + + var faker = new Faker().CustomInstantiator(f => + { + var x = new T(); + x.ModId = f.Hacker.Random.Hash(); + x.ModName = f.Name.FullName(); + x.ModAuthor = f.Internet.UserName(); + x.ModVersion = f.System.Version().ToString(); + x.ModDescription = f.Lorem.Sentences(3); + x.ModDll = f.System.FileName(".dll"); + x.ModIcon = f.System.CommonFileName(".png"); + x.ModR2RManagedDll32 = f.System.FileName(".dll").OrNull(f, 0.8f); + x.ModR2RManagedDll64 = f.System.FileName(".dll").OrNull(f, 0.8f); + x.ModNativeDll32 = f.System.FileName(".dll").OrNull(f, 0.8f); + x.ModNativeDll64 = f.System.FileName(".dll").OrNull(f, 0.8f); + x.IsLibrary = f.Random.Bool(0.01f); + x.ReleaseMetadataFileName = f.System.FileName(".json"); + x.IsLibrary = f.Random.Bool(0.01f); + x.ModDependencies = f.Random.ArrayElementsFast(dependencies, f.Random.Int(0, 5)); + x.OptionalDependencies = f.Random.ArrayElementsFast(dependencies, f.Random.Int(0, 1)); + x.SupportedAppId = f.Random.ArrayElementsFast(applications, 1); + return x; + }); + + return faker.GenerateArray(numConfigs); + } +} + +public class ModConfigMessage : ModConfig, IMessage, NullCompressor> +{ + public const int MessageType = 0; + + public SourceGeneratedSystemTextJsonSerializer GetSerializer() => new (ModConfigMessageContext.Default.ModConfigMessage); + public NullCompressor GetCompressor() => null; + public sbyte GetMessageType() => MessageType; +} + +public class ModConfigMessageWithDummySerializer : ModConfig, IMessage, NullCompressor> +{ + public const int MessageType = 0; + + public DummySerializer GetSerializer() => new(); + public NullCompressor GetCompressor() => null; + public sbyte GetMessageType() => MessageType; +} + +public class ModConfigMessageWithDummyCompressor: ModConfig, IMessage, DummyCompressor> +{ + public const int MessageType = 0; + + public DummySerializer GetSerializer() => new(); + public DummyCompressor GetCompressor() => new(); + public sbyte GetMessageType() => MessageType; +} + +[JsonSerializable(typeof(ModConfigMessage), GenerationMode = JsonSourceGenerationMode.Default)] +internal partial class ModConfigMessageContext : JsonSerializerContext { } + +[JsonSerializable(typeof(ModConfig), GenerationMode = JsonSourceGenerationMode.Default)] +internal partial class ModConfigContext : JsonSerializerContext { } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/ArrayExtensions.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/ArrayExtensions.cs new file mode 100644 index 0000000..d39c42b --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/ArrayExtensions.cs @@ -0,0 +1,14 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public static class ArrayExtensions +{ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetWithoutBoundsChecks(this T[] items, int index) + { + return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(items), index); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/Constants.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/Constants.cs new file mode 100644 index 0000000..b13edb6 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/Constants.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public static class Constants +{ + public const int DefaultOperationCount = 100000; +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyCompressor.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyCompressor.cs new file mode 100644 index 0000000..ce46d22 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyCompressor.cs @@ -0,0 +1,29 @@ +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +/// +/// Dummy compressor that performs no compression. +/// Use me when specifying TCompressor and return null in structures. +/// +public struct DummyCompressor : ICompressor +{ + /// + public int GetMaxCompressedSize(int inputSize) + { + return inputSize; + } + + /// + public int Compress(Span uncompressedData, Span compressedData) + { + uncompressedData.CopyTo(compressedData); + return uncompressedData.Length; + } + + /// + public void Decompress(Span compressedBuf, Span uncompressedBuf) + { + compressedBuf.CopyTo(uncompressedBuf); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyMessageHandlers.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyMessageHandlers.cs new file mode 100644 index 0000000..813a436 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyMessageHandlers.cs @@ -0,0 +1,21 @@ +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public class DummyMessageHandlerNoDeserialize : IMessageHandlerBase + where TStruct : IMessage + where TCompressor : ICompressor + where TSerializer : ISerializer +{ + public sbyte GetMessageType() => 0; + public void HandleMessage(Span data, int decompressedSize, ref TExtraData extraData) + { + // No code + } +} + +public struct DummyCallback : IMsgRefAction +{ + public void OnMessageReceive(ref ModConfigMessage received, ref int data) { } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/DummySerializer.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummySerializer.cs new file mode 100644 index 0000000..de15f24 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummySerializer.cs @@ -0,0 +1,16 @@ +using System.Buffers; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public struct DummySerializer : ISerializer where TStruct : new() +{ + public TStruct Deserialize(Span serialized) + { + return new TStruct(); + } + + public void Serialize(ref TStruct item, IBufferWriter writer) + { + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/FakerExtensions.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/FakerExtensions.cs new file mode 100644 index 0000000..ab6219c --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/FakerExtensions.cs @@ -0,0 +1,28 @@ +using System.Runtime; +using Bogus; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public static class FakerExtensions +{ + public static Random GlobalRandom = Random.Shared; + + public static T[] GenerateArray(this Faker faker, int num) where T : class + { + var items = GC.AllocateUninitializedArray(num); + for (int x = 0; x < items.Length; x++) + items[x] = faker.Generate(); + + return items; + } + + public static T[] ArrayElementsFast(this Randomizer random, T[] values, int num) where T : class + { + var elements = new T[num]; + + for (int x = 0; x < num; x++) + elements[x] = values[GlobalRandom.Next(0, num)]; + + return elements; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/SpanExtensions.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/SpanExtensions.cs new file mode 100644 index 0000000..24976f3 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/SpanExtensions.cs @@ -0,0 +1,28 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +/// +/// Extension methods related to spans. +/// +public static class SpanExtensions +{ + /// + /// Provides zero overhead unsafe array to span conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpanFast(this T[] data) + { + return MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(data), data.Length); + } + + /// + /// Provides zero overhead unsafe array to span conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpanFast(this T[] data, int length) + { + return MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(data), length); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj index bccd177..c61ff8b 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj @@ -1,30 +1,25 @@  - netstandard2.1;netstandard2.0 + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 + preview Sewer56 LICENSE.md Basic ZStandard compression implementation for Reloaded.Messaging based off of ZstdNet. Sewer56 https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true - 1.1.0 - + 2.0.0 true - - - - obj\Reloaded.Messaging.Compressor.ZStandard.xml - + NuGet-Icon.png - - obj\Reloaded.Messaging.Compressor.ZStandard.xml + true + true - + @@ -32,6 +27,10 @@ True + + True + \ + diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs b/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs index 3537063..003ab56 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs @@ -2,45 +2,56 @@ using Reloaded.Messaging.Interfaces; using ZstdNet; -namespace Reloaded.Messaging.Compressor.ZStandard +namespace Reloaded.Messaging.Compressor.ZStandard; + +/// +/// Creates a new compressor using ZStd. +/// +public struct ZStandardCompressor : ICompressor, IDisposable { - public class ZStandardCompressor : ICompressor, IDisposable + /// + /// Instance of the compressor. + /// + public readonly ZstdNet.Compressor Compressor; + + /// + /// Instance of the decompressor. + /// + public readonly Decompressor Decompressor; + + /// + /// Creates a new compressor based off of ZStandard. + /// + /// Sets the options used for compression. + /// Sets the options used for decompression. + public ZStandardCompressor(CompressionOptions compressionOptions = null, DecompressionOptions decompressionOptions = null) + { + Compressor = compressionOptions != null ? new ZstdNet.Compressor(compressionOptions) : new ZstdNet.Compressor(); + Decompressor = decompressionOptions != null ? new Decompressor(decompressionOptions) : new Decompressor(); + } + + /// + /// Creates a new compressor based off of ZStandard. + /// + public ZStandardCompressor() + { + Compressor = new ZstdNet.Compressor(); + Decompressor = new Decompressor(); + } + + /// + public void Dispose() { - public readonly ZstdNet.Compressor Compressor; - public readonly Decompressor Decompressor; - - public ZStandardCompressor(CompressionOptions compressionOptions = null, DecompressionOptions decompressionOptions = null) - { - Compressor = compressionOptions != null ? new ZstdNet.Compressor(compressionOptions) - : new ZstdNet.Compressor(); - - Decompressor = decompressionOptions != null ? new Decompressor(decompressionOptions) - : new Decompressor(); - } - - ~ZStandardCompressor() - { - Dispose(); - } - - public void Dispose() - { - Compressor?.Dispose(); - Decompressor?.Dispose(); - GC.SuppressFinalize(this); - } - - - /// - public byte[] Compress(byte[] data) - { - return Compressor.Wrap(data); - } - - /// - public byte[] Decompress(byte[] data) - { - return Decompressor.Unwrap(data); - } + Compressor?.Dispose(); + Decompressor?.Dispose(); } -} + + /// + public int GetMaxCompressedSize(int inputSize) => ZstdNet.Compressor.GetCompressBound(inputSize); + + /// + public int Compress(Span uncompressed, Span compressed) => Compressor.Wrap(uncompressed, compressed); + + /// + public void Decompress(Span compressedData, Span uncompressedData) => Decompressor.Unwrap(compressedData, uncompressedData); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Extras.Runtime/BrotliCompressor.cs b/Source/Reloaded.Messaging.Extras.Runtime/BrotliCompressor.cs new file mode 100644 index 0000000..672819b --- /dev/null +++ b/Source/Reloaded.Messaging.Extras.Runtime/BrotliCompressor.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.IO.Compression; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Extras.Runtime; + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +/// +/// Provides brotli compression support. +/// +public struct BrotliCompressor : ICompressor +{ + private byte _quality; + private byte _window; + + /// + /// Creates the default brotli compressor. + /// + public BrotliCompressor() + { + _window = 22; + _quality = 9; + } + + /// + /// Quality of encoder. Between 0 and 11. Recommend 9 for size/speed ratio. + /// Size of window. + public BrotliCompressor(byte quality, byte window = 22) + { + _quality = quality; + _window = window; + } + + /// + public int GetMaxCompressedSize(int inputSize) => BrotliEncoder.GetMaxCompressedLength(inputSize); + + /// + public int Compress(Span uncompressedData, Span compressedData) + { + using var encoder = new BrotliEncoder(_quality, _window); + encoder.Compress(uncompressedData, compressedData, out _, out var bytesWritten, true); + return bytesWritten; + } + + /// + public void Decompress(Span compressedBuf, Span uncompressedBuf) + { + using var decoder = new BrotliDecoder(); + decoder.Decompress(compressedBuf, uncompressedBuf, out _, out _); + } +} +#endif \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Extras.Runtime/Pool.cs b/Source/Reloaded.Messaging.Extras.Runtime/Pool.cs new file mode 100644 index 0000000..cf2f226 --- /dev/null +++ b/Source/Reloaded.Messaging.Extras.Runtime/Pool.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace Reloaded.Messaging.Extras.Runtime; + +internal static class Pool +{ + [ThreadStatic] + private static Utf8JsonWriter? _sharedWriter; + + internal static Utf8JsonWriter JsonWriterPerThread() + { + if (_sharedWriter == null) + { + _sharedWriter = new Utf8JsonWriter(Stream.Null); + return _sharedWriter; + } + + return _sharedWriter; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj b/Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj similarity index 59% rename from Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj rename to Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj index 4dd3ae0..18c4040 100644 --- a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj +++ b/Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj @@ -1,30 +1,35 @@  - netstandard2.0;net5.0;net7.0 - Reloaded.Messaging.Serializer.SystemTextJson + netcoreapp3.1;net6.0;net5.0 + preview Sewer56 Sewer56 - Basic Json serialization implementation for Reloaded.Messaging based off of System.Text.Json. + Provides support for compressors and serializers available in the .NET runtime. Sewer56 LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true true - 1.0.2 - + 2.0.0 + NuGet-Icon.png + enable + true - - - + true + true + + + True + \ + True diff --git a/Source/Reloaded.Messaging.Extras.Runtime/SourceGeneratedSystemTextJsonSerializer.cs b/Source/Reloaded.Messaging.Extras.Runtime/SourceGeneratedSystemTextJsonSerializer.cs new file mode 100644 index 0000000..b582ffd --- /dev/null +++ b/Source/Reloaded.Messaging.Extras.Runtime/SourceGeneratedSystemTextJsonSerializer.cs @@ -0,0 +1,45 @@ +namespace Reloaded.Messaging.Extras.Runtime; + +#if NET6_0_OR_GREATER +using System; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Reloaded.Messaging.Interfaces; +using System.Buffers; +using System.Runtime.CompilerServices; + +/// +public struct SourceGeneratedSystemTextJsonSerializer : ISerializer +{ + private readonly JsonTypeInfo _typeInfo; + + /// + /// Creates the System.Text.Json based serializer. + /// + /// Source generated JSON type information. + public SourceGeneratedSystemTextJsonSerializer(JsonTypeInfo typeInfo) + { + _typeInfo = typeInfo; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public TStruct Deserialize(Span serialized) + { + return JsonSerializer.Deserialize(serialized, _typeInfo)!; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public void Serialize(ref TStruct item, IBufferWriter writer) + { + var write = Pool.JsonWriterPerThread(); + write.Reset(writer); + JsonSerializer.Serialize(write, item, _typeInfo); + } +} +#endif \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs b/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs new file mode 100644 index 0000000..f3dd7bf --- /dev/null +++ b/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs @@ -0,0 +1,63 @@ +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text.Json; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Extras.Runtime; + +/// +public struct SystemTextJsonSerializer< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif +TStruct> : ISerializer +{ + /// + /// Serialization options. + /// + public JsonSerializerOptions Options { get; private set; } + + /// + /// Creates the System.Text.Json based serializer. + /// + public SystemTextJsonSerializer() + { + Options = new JsonSerializerOptions(); + } + + /// + /// Creates the System.Text.Json based serializer. + /// + /// Options to use for serialization/deserialization. + public SystemTextJsonSerializer(JsonSerializerOptions serializerOptions) + { + Options = serializerOptions; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Types passed to this serializer are preserved.")] +#endif + public TStruct Deserialize(Span serialized) + { + return JsonSerializer.Deserialize< + + TStruct > (serialized, Options)!; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Types passed to this serializer are preserved.")] +#endif + public void Serialize(ref TStruct item, IBufferWriter writer) + { + var write = Pool.JsonWriterPerThread(); + write.Reset(writer); + JsonSerializer.Serialize(write, item, Options); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibHost.cs b/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibHost.cs new file mode 100644 index 0000000..b6afabb --- /dev/null +++ b/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibHost.cs @@ -0,0 +1,138 @@ +using System; +using System.Runtime.CompilerServices; +using LiteNetLib; +using LiteNetLib.Utils; +using Reloaded.Messaging.Interfaces; +#if NET5_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace Reloaded.Messaging.Host.LiteNetLib; + +/// +/// Provides a simple client or host based off of LiteNetLib. +/// +public class LiteNetLibHost : IDisposable, IHost + where TDispatcher : IMessageDispatcher +{ + /// + /// The password necessary to join this host. If it does not match, incoming clients will be rejected. + /// + public string Password { get; set; } + + /// + /// Set to true to accept incoming clients, else reject all clients. + /// + public bool AcceptClients { get; set; } + + /// + /// Event for handling connection requests. + /// + public event EventBasedNetListener.OnConnectionRequest? ConnectionRequestEvent; + + /// + /// Dispatcher for individual messages sent to the client. + /// + public ref TDispatcher Dispatcher => ref _dispatcher; + + /// + /// Exposes the listener with which the manager was created. + /// + public EventBasedNetListener Listener { get; private set; } + + /// + /// The LiteNetLib manager. + /// + public NetManager Manager { get; private set; } + + /// + /// Provides access to first connected peer, useful if client. + /// + public NetPeer FirstPeer => Manager.FirstPeer; + + private TDispatcher _dispatcher; + + /// + /// Provides a simple client or host based off of LiteNetLib. + /// + /// The dispatcher used to send events to your callback handlers. + /// Set to true to accept incoming clients, else reject all requests. + /// The password necessary to join. + public LiteNetLibHost(bool acceptClients, TDispatcher dispatcher, string password = "") + { + _dispatcher = dispatcher; + Password = password; + AcceptClients = acceptClients; + Listener = new EventBasedNetListener(); + Listener.NetworkReceiveEvent += OnNetworkReceive; + Listener.ConnectionRequestEvent += ListenerOnConnectionRequestEvent; + + Manager = new NetManager(Listener); + Manager.UnsyncedEvents = true; + Manager.AutoRecycle = true; + } + + /// + ~LiteNetLibHost() => Dispose(); + + /// + public void Dispose() + { + Manager.Stop(); + GC.SuppressFinalize(this); + } + + private void ListenerOnConnectionRequestEvent(ConnectionRequest request) + { + if (ConnectionRequestEvent != null) + { + ConnectionRequestEvent(request); + return; + } + + if (AcceptClients) + request.AcceptIfKey(Password); + else + request.Reject(); + } + + + /// + public void SendFirstPeer(Span data) + { + FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); + } + + /// + public void SendToAll(Span data) + { + // TODO: This could be better optimised by sending a PR with Span overload in SendToAll + var peers = Manager.ConnectedPeerList; + for (int x = 0; x < peers.Count; x++) + peers[x].Send(data, DeliveryMethod.ReliableOrdered); + } + + // On each message received. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + private void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) + { + var data = AsSpanFast(reader.RawData, reader.Position, reader.AvailableBytes); + var state = new LiteNetLibState(peer, reader, channel, deliveryMethod); + Dispatcher.Dispatch(data, ref state); + } + + /// + /// Provides zero overhead unsafe array to span conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpanFast(byte[] data, int start, int length) + { +#if NET5_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(data), start), length); +#else + return data.AsSpan(start, length); +#endif + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibState.cs b/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibState.cs new file mode 100644 index 0000000..d63a237 --- /dev/null +++ b/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibState.cs @@ -0,0 +1,46 @@ +using LiteNetLib; + +namespace Reloaded.Messaging.Host.LiteNetLib; + +/// +/// Encapsulates the state of LiteNetLib at a given point in time. +/// +public struct LiteNetLibState +{ + /// + /// The peer from which the message was received. + /// + public NetPeer Peer { get; } + + /// + /// The reader for reading the raw data. + /// Here for completeness, you probably don't need it. + /// + public NetPacketReader Reader { get; } + + /// + /// The channel using which the message was delivered. + /// + public byte Channel { get; } + + /// + /// The method with which the message was delivered. + /// + public DeliveryMethod DeliveryMethod { get; } + + /// + /// Encapsulates the state of LiteNetLib. + /// + /// The peer from which the message was received. + /// The reader for reading the raw data. You probably don't need it. + /// The channel using which the message was delivered. + /// + + public LiteNetLibState(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) + { + Peer = peer; + Reader = reader; + Channel = channel; + DeliveryMethod = deliveryMethod; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj b/Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj new file mode 100644 index 0000000..8287279 --- /dev/null +++ b/Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj @@ -0,0 +1,46 @@ + + + + netstandard2.1;netcoreapp3.1;net5.0 + false + preview + Sewer56 + + Provides support for LiteNetLib as a host for Reloaded.Messaging. + LICENSE.md + https://github.com/Reloaded-Project/Reloaded.Messaging + https://github.com/Reloaded-Project/Reloaded.Messaging + true + Sewer56 + 2.0.0 + + true + 1701;1702;NU5104 + true + NuGet-Icon.png + enable + + true + true + + + + + + + + + True + + + + True + \ + + + + + + + + diff --git a/Source/Reloaded.Messaging.Interfaces/ICompressor.cs b/Source/Reloaded.Messaging.Interfaces/ICompressor.cs index 33afa45..a5084cf 100644 --- a/Source/Reloaded.Messaging.Interfaces/ICompressor.cs +++ b/Source/Reloaded.Messaging.Interfaces/ICompressor.cs @@ -1,20 +1,31 @@ -namespace Reloaded.Messaging.Interfaces +using System; + +namespace Reloaded.Messaging.Interfaces; + +/// +/// Defines the minimal interface necessary to bootstrap a 3rd party compressor. +/// +public interface ICompressor { /// - /// Defines the minimal interface necessary to bootstrap a 3rd party compressor. + /// Gets the maximum possible size of a compressed file. /// - public interface ICompressor - { - /// - /// Compresses the provided byte array. - /// - /// The data to compress. - byte[] Compress(byte[] data); + /// The input size. + /// The maximum possible size after compression. + int GetMaxCompressedSize(int inputSize); - /// - /// Decompresses the provided byte array. - /// - /// The data to decompress. - byte[] Decompress(byte[] data); - } -} + /// + /// Compresses the provided byte array. + /// + /// The data to compress. + /// The data to compress. + /// Number of compressed bytes. + int Compress(Span uncompressedData, Span compressedData); + + /// + /// Decompresses the provided byte array. + /// + /// The data to decompress. + /// The buffer containing uncompressed data. + void Decompress(Span compressedBuf, Span uncompressedBuf); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/IHost.cs b/Source/Reloaded.Messaging.Interfaces/IHost.cs new file mode 100644 index 0000000..0ef01fb --- /dev/null +++ b/Source/Reloaded.Messaging.Interfaces/IHost.cs @@ -0,0 +1,25 @@ +using System; +using System.Net; + +namespace Reloaded.Messaging.Interfaces; + +/// +/// Encapsulates an individual host. +/// +public interface IHost where TDispatcher : IMessageDispatcher +{ + /// + /// The message dispatcher owned by this host instance. + /// + public ref TDispatcher Dispatcher { get; } + + /// + /// Sends a message to the first peer (i.e. client to host). + /// + public void SendFirstPeer(Span data); + + /// + /// Sends a message to all connected peers (i.e. host to clients). + /// + public void SendToAll(Span data); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/IMessage.cs b/Source/Reloaded.Messaging.Interfaces/IMessage.cs index 9d40916..6cedf4e 100644 --- a/Source/Reloaded.Messaging.Interfaces/IMessage.cs +++ b/Source/Reloaded.Messaging.Interfaces/IMessage.cs @@ -1,15 +1,16 @@ using Reloaded.Messaging.Interfaces.Message; -namespace Reloaded.Messaging.Interfaces +namespace Reloaded.Messaging.Interfaces; + +/// +/// Common interface shared by individual messages. +/// +public interface IMessage : ISerializable + where TSerializer : ISerializer + where TCompressor : ICompressor { /// - /// Common interface shared by individual messages. + /// Returns the unique message type/id for this message. /// - public interface IMessage : ISerializable where TMessageType : unmanaged - { - /// - /// Returns the unique message type/id for this message. - /// - TMessageType GetMessageType(); - } -} + sbyte GetMessageType(); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/IMessageDispatcher.cs b/Source/Reloaded.Messaging.Interfaces/IMessageDispatcher.cs new file mode 100644 index 0000000..5851ddd --- /dev/null +++ b/Source/Reloaded.Messaging.Interfaces/IMessageDispatcher.cs @@ -0,0 +1,34 @@ +using System; + +namespace Reloaded.Messaging.Interfaces; + +/// +/// Encapsulates the minimal interface required by a message dispatcher. +/// +/// The extra data +public interface IMessageDispatcher +{ + /// + /// Gets a handler that handles a specific message type. + /// + /// The type of message requested. + /// Handler for the specific message. Might be null. + ref IMessageHandlerBase? GetHandlerForType(byte messageType); + + /// + /// Sets a handler for a specific message type. + /// + void AddOrOverrideHandler(IMessageHandlerBase handler); + + /// + /// Removes a handler assigned to a specific message type. + /// + void RemoveHandler(byte messageType); + + /// + /// Given a raw network message, decodes the message and delegates it to an appropriate handling method. + /// + /// Data containing a packed Reloaded.Messaging message. + /// The extra data associated with this request. + void Dispatch(Span data, ref TExtraData extraData); +} diff --git a/Source/Reloaded.Messaging.Interfaces/IMessageHandlerBase.cs b/Source/Reloaded.Messaging.Interfaces/IMessageHandlerBase.cs new file mode 100644 index 0000000..9e6405b --- /dev/null +++ b/Source/Reloaded.Messaging.Interfaces/IMessageHandlerBase.cs @@ -0,0 +1,23 @@ +using System; + +namespace Reloaded.Messaging.Interfaces; + +/// +/// Common interface shared by types that perform direct deserialization and handling of messages. +/// +/// Type of extra data attached to the message. Usually state of network library. +public interface IMessageHandlerBase +{ + /// + /// Returns the unique message type/id for this message. + /// + sbyte GetMessageType(); + + /// + /// Handles a specific incoming message. + /// + /// The raw data, without message header, ready for decompression and deserialization. + /// Expected size after decompression. + /// Extra data. Usually state of networking library used. + void HandleMessage(Span data, int decompressedSize, ref TExtraData extraData); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/ISerializer.cs b/Source/Reloaded.Messaging.Interfaces/ISerializer.cs index 06e96fd..56b369e 100644 --- a/Source/Reloaded.Messaging.Interfaces/ISerializer.cs +++ b/Source/Reloaded.Messaging.Interfaces/ISerializer.cs @@ -1,22 +1,24 @@ -namespace Reloaded.Messaging.Interfaces +using System; +using System.Buffers; + +namespace Reloaded.Messaging.Interfaces; + +/// +/// Defines the minimal interface necessary to bootstrap a 3rd party serializer. +/// +public interface ISerializer { /// - /// Defines the minimal interface necessary to bootstrap a 3rd party serializer. + /// Deserializes the provided byte array into a concrete type. /// - public interface ISerializer - { - /// - /// Deserializes the provided byte array into a concrete type. - /// - /// The type of the structure to deserialize. - /// The data to deserialize. - TStruct Deserialize(byte[] serialized); + /// The data to deserialize. + TStruct Deserialize(Span serialized); - /// - /// Serializes the provided item into a byte array. - /// - /// The item to serialize to bytes. - /// Serialized item. - byte[] Serialize(ref TStruct item); - } -} + /// + /// Serializes the provided item into a byte array. + /// + /// The item to serialize to bytes. + /// The writer into which the serialized message should be written to. + /// Serialized item. + void Serialize(ref TStruct item, IBufferWriter writer); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs b/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs index 1d00d00..b08db67 100644 --- a/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs +++ b/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs @@ -1,18 +1,18 @@ -namespace Reloaded.Messaging.Interfaces.Message +namespace Reloaded.Messaging.Interfaces.Message; + +/// +/// An interface that provides serialization/deserialization and compression/decompression support for. +/// +public interface ISerializable where TSerializer : ISerializer + where TCompressor : ICompressor { /// - /// An interface that provides serialization/deserialization and compression/decompression support for. + /// Returns the serializer for this specific type. /// - public interface ISerializable - { - /// - /// Returns the serializer for this specific type. - /// - ISerializer GetSerializer(); + TSerializer GetSerializer(); - /// - /// Returns the compressor for this specific type. - /// - ICompressor GetCompressor(); - } + /// + /// Returns the compressor for this specific type. + /// + TCompressor? GetCompressor(); } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj b/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj index fafc13e..2d56eac 100644 --- a/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj +++ b/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj @@ -1,22 +1,35 @@  - netstandard2.0 + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 + preview Contains all of the interfaces (and some extension functionality) used by the base Reloaded.Messaging library. This package exists to allow you to use various features of the library, such as serializers without the need to import the dependencies of the base package. true LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 true + NuGet-Icon.png + 2.0.0 + $(DefineConstants);USE_NATIVE_SPAN_API + true + enable + + + + True + + True + \ + diff --git a/Source/Reloaded.Messaging.Interfaces/Serializable.cs b/Source/Reloaded.Messaging.Interfaces/Serializable.cs deleted file mode 100644 index 2a40213..0000000 --- a/Source/Reloaded.Messaging.Interfaces/Serializable.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Runtime.CompilerServices; -using Reloaded.Messaging.Interfaces.Message; - -namespace Reloaded.Messaging.Interfaces -{ - /// - /// An extension class providing serialization support to implementers of . - /// - public static class Serializable - { - /// - /// Serializes and compresses the current instance of the class or struct - /// using the serializer and compressor defined by the . - /// - public static byte[] Serialize(this TSerializable serializable) where TSerializable : ISerializable - { - var serializer = serializable.GetSerializer(); - var compressor = serializable.GetCompressor(); - - byte[] serialized = serializer.Serialize(ref serializable); - if (compressor != null) - return compressor.Compress(serialized); - - return serialized; - } - - /// - /// Decompresses and deserializes the current instance of the class or struct using the - /// serializer and compressor defined by the . - /// - public static ISerializable Deserialize(this TType serializable, byte[] bytes) where TType : ISerializable - { - var compressor = serializable.GetCompressor(); - var serializer = serializable.GetSerializer(); - - byte[] decompressed = bytes; - if (compressor != null) - decompressed = compressor.Decompress(bytes); - - return serializer.Deserialize(decompressed); - } - - /// - /// Decompresses and deserializes the current instance of the class or struct using the - /// serializer and compressor defined by the . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ISerializable Deserialize(byte[] bytes) where TType : ISerializable, new() - { - var serializable = new TType(); - return Deserialize(serializable, bytes); - } - } -} diff --git a/Source/Reloaded.Messaging.Interfaces/Utilities/NullCompressor.cs b/Source/Reloaded.Messaging.Interfaces/Utilities/NullCompressor.cs new file mode 100644 index 0000000..5baa4b1 --- /dev/null +++ b/Source/Reloaded.Messaging.Interfaces/Utilities/NullCompressor.cs @@ -0,0 +1,29 @@ +using System; + +namespace Reloaded.Messaging.Interfaces.Utilities; + +/// +/// Dummy compressor that performs no compression. +/// Use me when specifying TCompressor and return null in structures. +/// +public class NullCompressor : ICompressor +{ + /// + public int GetMaxCompressedSize(int inputSize) + { + return inputSize; + } + + /// + public int Compress(Span uncompressedData, Span compressedData) + { + uncompressedData.CopyTo(compressedData); + return uncompressedData.Length; + } + + /// + public void Decompress(Span compressedBuf, Span uncompressedBuf) + { + compressedBuf.CopyTo(uncompressedBuf); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/MessagePackSerializer.cs b/Source/Reloaded.Messaging.Serializer.MessagePack/MessagePackSerializer.cs new file mode 100644 index 0000000..1a3f886 --- /dev/null +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/MessagePackSerializer.cs @@ -0,0 +1,60 @@ +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using MessagePack; +using MessagePack.Resolvers; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Serializer.MessagePack; + +/// +/// Serializer that uses MessagePack. +/// +public struct MessagePackSerializer : ISerializer +{ + /// + /// Options for the MessagePack serializer. + /// + public MessagePackSerializerOptions SerializerOptions { get; private set; } = MessagePackSerializerOptions.Standard; + + /// + /// Creates a new instance of the MessagePack serializer. + /// + public MessagePackSerializer() + { + SerializerOptions = SerializerOptions.WithResolver(ContractlessStandardResolver.Instance); + } + + /// + /// Creates a new instance of the MessagePack serializer. + /// + /// + /// Custom resolver to pass to MessagePack, default instance uses "Contractless Resolver". + /// + public MessagePackSerializer(IFormatterResolver? resolver = null) + { + SerializerOptions = SerializerOptions.WithResolver(resolver ?? ContractlessStandardResolver.Instance); + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public unsafe TStruct Deserialize(Span serialized) + { + fixed (byte* dataPtr = &serialized[0]) + { + // We have to tank potential heap allocation here. + // Hoping JIT escape analysis is smart enough not to heap allocate this one. + var manager = new UnmanagedMemoryManager(dataPtr, serialized.Length); + return MessagePackSerializer.Deserialize(manager.Memory, SerializerOptions); + } + } + + /// + public void Serialize(ref TStruct item, IBufferWriter writer) + { + MessagePackSerializer.Serialize(writer, item, SerializerOptions); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs b/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs deleted file mode 100644 index c6b39e2..0000000 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs +++ /dev/null @@ -1,50 +0,0 @@ -using MessagePack; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Serializer.MessagePack -{ - public class MsgPackSerializer : ISerializer - { - /// - /// Uses LZ4 compression for serialization. - /// - public bool UseLZ4 { get; private set; } - - /// - /// Any custom resolver to pass to MessagePack. - /// Default is - /// - public IFormatterResolver Resolver { get; private set; } = global::MessagePack.Resolvers.ContractlessStandardResolver.Instance; - - /// - /// Creates a new instance of the MessagePack serializer. - /// - /// Uses MessagePack's serializer with LZ4 compression. - /// - /// Custom resolver to pass to MessagePack, default is "Contractless Resolver" - /// (). - /// - public MsgPackSerializer(bool useLz4, IFormatterResolver resolver = null) - { - UseLZ4 = useLz4; - if (resolver != null) - Resolver = resolver; - } - - - /// - public TStruct Deserialize(byte[] serialized) - { - return UseLZ4 ? LZ4MessagePackSerializer.Deserialize(serialized, Resolver) : - MessagePackSerializer.Deserialize(serialized, Resolver); - } - - - /// - public byte[] Serialize(ref TStruct item) - { - return UseLZ4 ? LZ4MessagePackSerializer.Serialize(item, Resolver) : - MessagePackSerializer.Serialize(item, Resolver); - } - } -} diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj index b6eacd5..26349b2 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj @@ -1,32 +1,28 @@  - netstandard2.1;netstandard2.0 - Reloaded.Messaging.Serializer.MessagePack + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 + preview Sewer56 Sewer56 Basic MessagePack serialization implementation for Reloaded.Messaging based off of MessagePack-CSharp. Sewer56 LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true - 1.1.0 - + 2.0.0 true - - - - obj\Reloaded.Messaging.Serializer.MessagePack.xml - + NuGet-Icon.png + true + enable - - obj\Reloaded.Messaging.Serializer.MessagePack.xml + false + true - + @@ -34,6 +30,10 @@ True + + True + \ + diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/UnmanagedMemoryManager.cs b/Source/Reloaded.Messaging.Serializer.MessagePack/UnmanagedMemoryManager.cs new file mode 100644 index 0000000..88e6958 --- /dev/null +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/UnmanagedMemoryManager.cs @@ -0,0 +1,52 @@ +using System; +using System.Buffers; + +namespace Reloaded.Messaging.Serializer.MessagePack; + +/// +/// A MemoryManager over a raw pointer, used for conversion to for the serializers that require it. +/// Pointers passed to this method are expected to be externally pinned. +/// +public sealed unsafe class UnmanagedMemoryManager : MemoryManager where T : unmanaged +{ + private T* _pointer; + private int _length; + + /// + /// Creates a UnmanagedMemoryManager from pointer and size. + /// + public UnmanagedMemoryManager(T* pointer, int length) + { + _pointer = pointer; + _length = length; + } + + /// + /// Updates the values behind the current instance. + /// + public void Update(T* pointer, int length) + { + _pointer = pointer; + _length = length; + } + + /// + /// Obtains a span that represents the region. + /// + public override Span GetSpan() => new(_pointer, _length); + + /// + /// Returns pointer representing the data [no pin occurs]. + /// + public override MemoryHandle Pin(int elementIndex = 0) => new(_pointer + elementIndex); + + /// + /// Has no effect. + /// + public override void Unpin() { } + + /// + /// Releases all resources associated with this object + /// + protected override void Dispose(bool disposing) { } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs deleted file mode 100644 index 86d9eaf..0000000 --- a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Runtime.Serialization; -using System.Text; -using Newtonsoft.Json; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Serializer.NewtonsoftJson -{ - public class NewtonsoftJsonSerializer : ISerializer - { - /// - /// Serialization options. - /// - public JsonSerializerSettings Options { get; private set; } - - /// - /// Creates the System.Text.Json based serializer. - /// - /// Options to use for serialization/deserialization. - public NewtonsoftJsonSerializer(JsonSerializerSettings serializerOptions) - { - Options = serializerOptions; - } - - /// - public TStruct Deserialize(byte[] serialized) - { - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(serialized), Options); - } - - /// - public byte[] Serialize(ref TStruct item) - { - return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(item, Options)); - } - } -} diff --git a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj deleted file mode 100644 index eb9ee73..0000000 --- a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - netstandard2.0 - Reloaded.Messaging.Serializer.NewtonsoftJson - Sewer56 - Sewer56 - Basic Json serialization implementation for Reloaded.Messaging based off of Newtonsoft.Json. - Sewer56 - LICENSE.md - https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 - https://github.com/Reloaded-Project/Reloaded.Messaging - true - true - 1.0.1 - - - - - - - - - - - - - True - - - - - diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj index 9ebc6dc..0e9bed1 100644 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj @@ -1,33 +1,33 @@  - netstandard2.1;netstandard2.0 + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 + preview Sewer56 Reloaded.Messaging.Serializer.ReloadedMemory - Basic Reloaded.Memory based serialization implementation for Reloaded.Messaging that converts structs to their raw byte representation and back. + Basic Reloaded.Memory based serialization implementation for Reloaded.Messaging, for those happy with manual serialization or converting to raw bytes. LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true - 1.1.0 - + 2.0.0 true - - - - obj\Reloaded.Messaging.Serializer.ReloadedMemory.xml - + NuGet-Icon.png + true - - obj\Reloaded.Messaging.Serializer.ReloadedMemory.xml + true + true - + + + True + \ + True diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs deleted file mode 100644 index 414b0a7..0000000 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Reloaded.Memory; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Serializer.ReloadedMemory -{ - public class ReloadedMemorySerializer : ISerializer - { - /// - /// Marshals structures if set to true however is significantly slower. - /// Note: Marshalling also allows you to serialize Classes with [StructLayout] attribute. - /// - public bool MarshalValues { get; private set; } - - /// - /// Creates the Reloaded.Memory based serializer. - /// - /// - /// Marshals structures if set to true however is significantly slower. - /// Note: Marshalling also allows you to serialize Classes with [StructLayout] attribute. - /// - public ReloadedMemorySerializer(bool marshalValues) - { - MarshalValues = marshalValues; - } - - /// - public TStruct Deserialize(byte[] serialized) - { - Struct.FromArray(serialized, out TStruct value, MarshalValues, 0); - return value; - } - - /// - public byte[] Serialize(ref TStruct item) - { - byte[] bytes = Struct.GetBytes(ref item, MarshalValues); - return bytes; - } - } -} diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/UnmanagedReloadedMemorySerializer.cs b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/UnmanagedReloadedMemorySerializer.cs new file mode 100644 index 0000000..cf98303 --- /dev/null +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/UnmanagedReloadedMemorySerializer.cs @@ -0,0 +1,37 @@ +using System; +using System.Buffers; +using Reloaded.Memory; +using Reloaded.Messaging.Interfaces; + +#if NET5_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif + +namespace Reloaded.Messaging.Serializer.ReloadedMemory; + +/// +/// Serializes messages using raw byte conversion with Reloaded.Memory. +/// +public unsafe struct UnmanagedReloadedMemorySerializer : ISerializer where TStruct : unmanaged +{ + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public TStruct Deserialize(Span serialized) + { + Struct.FromArray(serialized, out TStruct value); + return value; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public void Serialize(ref TStruct item, IBufferWriter writer) + { + var span = writer.GetSpan(sizeof(TStruct)); + Struct.GetBytes(ref item, span); + writer.Advance(sizeof(TStruct)); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs deleted file mode 100644 index 1f28018..0000000 --- a/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Text.Json; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Serializer.SystemTextJson -{ - /// - public class SystemTextJsonSerializer : ISerializer - { - /// - /// Serialization options. - /// - public JsonSerializerOptions Options { get; private set; } - - /// - /// Creates the System.Text.Json based serializer. - /// - /// Options to use for serialization/deserialization. - public SystemTextJsonSerializer(JsonSerializerOptions serializerOptions) - { - Options = serializerOptions; - } - - /// - public TStruct Deserialize(byte[] serialized) - { - return JsonSerializer.Deserialize(serialized, Options); - } - - /// - public byte[] Serialize(ref TStruct item) - { - return JsonSerializer.SerializeToUtf8Bytes(item, Options); - } - } -} diff --git a/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs b/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs deleted file mode 100644 index 58f5a5a..0000000 --- a/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Net; - -namespace Reloaded.Messaging.Tests.Init -{ - public class TestingHosts : IDisposable - { - private const string DefaultPassword = "CutenessIsJustice"; - public SimpleHost SimpleServer; - public SimpleHost SimpleClient; - - public TestingHosts() - { - SimpleServer = new SimpleHost(true, DefaultPassword); - SimpleClient = new SimpleHost(false, DefaultPassword); - - SimpleServer.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); - SimpleClient.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); - SimpleClient.NetManager.Connect(new IPEndPoint(IPAddress.Loopback, SimpleServer.NetManager.LocalPort), DefaultPassword); - -#if DEBUG - SimpleServer.NetManager.DisconnectTimeout = int.MaxValue; - SimpleClient.NetManager.DisconnectTimeout = int.MaxValue; -#endif - } - - public void Dispose() - { - SimpleServer?.Dispose(); - SimpleClient?.Dispose(); - } - } -} diff --git a/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs b/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs new file mode 100644 index 0000000..f19e499 --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Net; +using LiteNetLib; +using System.Threading.Tasks; +using Reloaded.Messaging.Host.LiteNetLib; +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Tests.Messages; +using Reloaded.Messaging.Utilities; +using Xunit; + +namespace Reloaded.Messaging.Tests; + +public class TestingHosts : IDisposable +{ + private const string DefaultPassword = "SwagSwagSwag"; + public LiteNetLibHost> Server; + public LiteNetLibHost> Client; + + public TestingHosts() + { + Server = new LiteNetLibHost>(true, new MessageDispatcher(), DefaultPassword); + Client = new LiteNetLibHost>(true, new MessageDispatcher(), DefaultPassword); + + Server.Manager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); + Client.Manager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); + Client.Manager.Connect(new IPEndPoint(IPAddress.Loopback, Server.Manager.LocalPort), DefaultPassword); + +#if DEBUG + Server.Manager.DisconnectTimeout = int.MaxValue; + Client.Manager.DisconnectTimeout = int.MaxValue; +#endif + } + + public void Dispose() + { + Server?.Dispose(); + Client?.Dispose(); + } + + [Fact(Timeout = 20000)] + public async Task SendAndReceiveMessage() + { + // Arrange/Setup Host + var messageHandler = new LiteNetLibMessageHandler(); + messageHandler.Received.AddToDispatcher(messageHandler, ref Server.Dispatcher); + + // Send sample message. + var sample = new Vector3(0.0f, 1.0f, 2.0f); + using var serialized = sample.Serialize(ref sample); + Client.SendFirstPeer(serialized.Span); + + // Wait for response. + while (messageHandler.Received.Equals(default)) + await Task.Delay(16); + + Assert.Equal(sample, messageHandler.Received); + Assert.NotNull(messageHandler.State.Peer); + Assert.NotNull(messageHandler.State.Reader); + } + + public class LiteNetLibMessageHandler : IMsgRefAction + { + public Vector3 Received; + public LiteNetLibState State; + + public void OnMessageReceive(ref Vector3 received, ref LiteNetLibState data) + { + Received = received; + State = data; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/MessageCreationTests.cs b/Source/Reloaded.Messaging.Tests/MessageCreationTests.cs new file mode 100644 index 0000000..108d03d --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/MessageCreationTests.cs @@ -0,0 +1,133 @@ +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Tests.Messages; +using System; +using Reloaded.Messaging.Compressor.ZStandard; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces; +using Xunit; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Serializer.MessagePack; +using Reloaded.Messaging.Serializer.ReloadedMemory; + +namespace Reloaded.Messaging.Tests; + +public class MessageCreationTests +{ + #region Baseline Tests + [Fact] + public unsafe void CreateMessage_Baseline_WithReloadedMemoryNoCompression() + { + // Arrange + var structure = new Vector3(0.5f, 1.0f, 2.0f); + CreateMessage_Serializer_Common, NullCompressor>(ref structure); + } + + [Fact] + public unsafe void CreateMessage_Baseline_WithReloadedMemoryDummyCompression() + { + // Arrange + var structure = new Vector3ReloadedMemoryDummyCompression(0.5f, 1.0f, 2.0f); + using var message = MessageWriter, NullCompressor>.Serialize(ref structure); + + // Act & Assert + var sizeEqual = (sizeof(Vector3) + HeaderReader.CompressedHeaderSize) == message.Span.Length; + Assert.True(sizeEqual, "Size after packing does not match expected size."); + + HeaderReader.ReadHeader(message.Span, out var messageType, out var compressedSize, out var headerSize); + Assert.True(structure.GetMessageType().Equals(messageType), "Invalid message type."); + Assert.True(compressedSize == sizeof(Vector3ReloadedMemoryDummyCompression), "Compressed size should return invalid if no compression is used."); + + var deserialize = new MessageReader, NullCompressor>(in structure); + var deserialized = deserialize.Deserialize(message.Span.Slice(headerSize), compressedSize); + Assert.True(structure.Equals(deserialized), "Vectors should be equal after deserialize."); + } + #endregion + + #region Serializer Tests + [Fact] + public unsafe void CreateMessage_WithMessagePack() + { + // Arrange + var structure = new Vector3MessagePackNoCompression(0.5f, 1.0f, 2.0f); + CreateMessage_Serializer_Common, NullCompressor>(ref structure); + } + + [Fact] + public unsafe void CreateMessage_WithSystemTextJson() + { + // Arrange + var structure = new Vector3SystemTextJson(0.5f, 1.0f, 2.0f); + CreateMessage_Serializer_Common, NullCompressor>(ref structure); + } + +#if NET6_0_OR_GREATER + [Fact] + public unsafe void CreateMessage_WithSystemTextJsonSourceGen() + { + // Arrange + var structure = new Vector3SystemTextJsonSourceGenerated(0.5f, 1.0f, 2.0f); + CreateMessage_Serializer_Common, NullCompressor>(ref structure); + } +#endif + + #endregion + + #region Compressor Tests + + [Fact] + public unsafe void CreateMessage_WithZStandard() + { + // Arrange + var structure = new Vector3ZStandard(0.5f, 1.0f, 2.0f); + CreateMessage_Compressor_Common, ZStandardCompressor>(ref structure); + } + + [Fact] + public unsafe void CreateMessage_WithBrotli() + { + // Arrange + var structure = new Vector3Brotli(0.5f, 1.0f, 2.0f); + CreateMessage_Compressor_Common, BrotliCompressor>(ref structure); + } + + #endregion + + /// + /// Common function for testing serializers. + /// + private unsafe void CreateMessage_Serializer_Common(ref TStruct value) where TStruct : IMessage, IEquatable, new() + where TSerializer : ISerializer + where TCompressor : ICompressor + { + // Arrange + using var message = MessageWriter.Serialize(ref value); + + // Act & Assert + HeaderReader.ReadHeader(message.Span, out var messageType, out var sizeAfterDecompression, out var headerSize); + Assert.True(value.GetMessageType().Equals(messageType), "Invalid message type."); + Assert.True(sizeAfterDecompression == -1, "Compressed size should return invalid if no compression is used."); + + var deserialize = new MessageReader(in value); + var deserialized = deserialize.Deserialize(message.Span.Slice(headerSize), -1); + Assert.True(value.Equals(deserialized), "Values should be equal after deserialize."); + } + + /// + /// Common function for testing serializers. + /// + private unsafe void CreateMessage_Compressor_Common(ref TStruct value) where TStruct : IMessage, new() + where TSerializer : ISerializer + where TCompressor : ICompressor + { + // Arrange + using var message = MessageWriter.Serialize(ref value); + + // Act & Assert + HeaderReader.ReadHeader(message.Span, out var messageType, out var sizeAfterDecompression, out var headerSize); + Assert.True(value.GetMessageType().Equals(messageType), "Invalid message type."); + + var deserialize = new MessageReader(in value); + var deserialized = deserialize.Deserialize(message.Span.Slice(headerSize), sizeAfterDecompression); + Assert.True(value.Equals(deserialized), "Values should be equal after deserialize."); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/MessageDispatcherTests.cs b/Source/Reloaded.Messaging.Tests/MessageDispatcherTests.cs new file mode 100644 index 0000000..d4cb75b --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/MessageDispatcherTests.cs @@ -0,0 +1,88 @@ +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Tests.Messages; +using Reloaded.Messaging.Utilities; +using Xunit; + +namespace Reloaded.Messaging.Tests; + +/// +/// Provides various tests for the high end message handler. +/// +public class MessageDispatcherTests +{ + [Fact] + public void Dispatch_GetHandler_WithNoHandler_ReturnsNull() + { + // Arrange + var dispatcher = new MessageDispatcher(); + + // Check all slots + for (int x = 0; x < sbyte.MaxValue; x++) + Assert.Null(dispatcher.GetHandlerForType((byte)x)); + } + + [Fact] + public void Dispatch_WithHandler_ReturnNotNull() + { + // Arrange + var dispatcher = new MessageDispatcher(); + var sample = new Vector3(0.5f, 1.0f, 2.0f); + + // Add to dispatcher. + var receiveAction = new Vector3ReceiveAction(); + sample.AddToDispatcher(receiveAction, ref dispatcher); + + // Pack message. + Assert.NotNull(dispatcher.GetHandlerForType((byte)sample.GetMessageType())); + } + + [Fact] + public void Dispatch_WithRemovedHandler_ReturnNull() + { + // Arrange + var dispatcher = new MessageDispatcher(); + var sample = new Vector3(0.5f, 1.0f, 2.0f); + + // Add to dispatcher. + var receiveAction = new Vector3ReceiveAction(); + sample.AddToDispatcher(receiveAction, ref dispatcher); + + // Check for presence, then after removal. + Assert.NotNull(dispatcher.GetHandlerForType((byte)sample.GetMessageType())); + dispatcher.RemoveHandler((byte)sample.GetMessageType()); + Assert.Null(dispatcher.GetHandlerForType((byte)sample.GetMessageType())); + } + + [Fact] + public void Dispatch_WithHandler_ReceivesMessage() + { + // Arrange + var dispatcher = new MessageDispatcher(); + var sample = new Vector3(0.5f, 1.0f, 2.0f); + + // Add to dispatcher. + var receiveAction = new Vector3ReceiveAction(); + sample.AddToDispatcher(receiveAction, ref dispatcher); + + // Pack message. + using var serialized = sample.Serialize(ref sample); + var extraData = 42; + dispatcher.Dispatch(serialized.Span, ref extraData); + + // Check message was received. + Assert.Equal(extraData, receiveAction.ExtraData); + Assert.Equal(sample, receiveAction.Received); + } + + public class Vector3ReceiveAction : IMsgRefAction + { + public Vector3 Received; + public int ExtraData; + + public void OnMessageReceive(ref Vector3 received, ref int data) + { + Received = received; + ExtraData = data; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/MessageType.cs b/Source/Reloaded.Messaging.Tests/MessageType.cs deleted file mode 100644 index 686c815..0000000 --- a/Source/Reloaded.Messaging.Tests/MessageType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Reloaded.Messaging.Tests -{ - public enum MessageType : byte - { - String, - Vector3 - } -} diff --git a/Source/Reloaded.Messaging.Tests/Messages/MessageType.cs b/Source/Reloaded.Messaging.Tests/Messages/MessageType.cs new file mode 100644 index 0000000..c4d7ebb --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/MessageType.cs @@ -0,0 +1,11 @@ +namespace Reloaded.Messaging.Tests.Messages; + +public enum MessageType : byte +{ + Vector3, + Vector3ReloadedMemoryDummyCompression, + Vector3MessagePackNoCompression, + Vector3ZStandard, + Vector3SystemTextJson, + Vector3Brotli +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3.cs new file mode 100644 index 0000000..b80136f --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3.cs @@ -0,0 +1,52 @@ +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Serializer.ReloadedMemory; +using System; +using Reloaded.Messaging.Interfaces.Utilities; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3 : IMessage, NullCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3; + public UnmanagedReloadedMemorySerializer GetSerializer() => new(); + public NullCompressor? GetCompressor() => null; + + public float X; + public float Y; + public float Z; + + public Vector3(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3 other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3Brotli.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3Brotli.cs new file mode 100644 index 0000000..214e543 --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3Brotli.cs @@ -0,0 +1,53 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Compressor.ZStandard; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Serializer.ReloadedMemory; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3Brotli : IMessage, BrotliCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3Brotli; + public UnmanagedReloadedMemorySerializer GetSerializer() => new(); + public BrotliCompressor GetCompressor() => new(); + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3Brotli(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3Brotli other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3Brotli)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3MessagePackNoCompression.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3MessagePackNoCompression.cs new file mode 100644 index 0000000..cd2d595 --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3MessagePackNoCompression.cs @@ -0,0 +1,52 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Serializer.MessagePack; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3MessagePackNoCompression : IMessage, NullCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3MessagePackNoCompression; + public MessagePackSerializer GetSerializer() => new(); + public NullCompressor? GetCompressor() => default; + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3MessagePackNoCompression(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3MessagePackNoCompression other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3MessagePackNoCompression)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3ReloadedMemoryDummyCompression.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3ReloadedMemoryDummyCompression.cs new file mode 100644 index 0000000..71202c4 --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3ReloadedMemoryDummyCompression.cs @@ -0,0 +1,52 @@ +using System; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Serializer.ReloadedMemory; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3ReloadedMemoryDummyCompression : IMessage, NullCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3ReloadedMemoryDummyCompression; + public UnmanagedReloadedMemorySerializer GetSerializer() => new(); + public NullCompressor? GetCompressor() => new(); + + public float X; + public float Y; + public float Z; + + public Vector3ReloadedMemoryDummyCompression(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3ReloadedMemoryDummyCompression other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3ReloadedMemoryDummyCompression)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJson.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJson.cs new file mode 100644 index 0000000..5e5969a --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJson.cs @@ -0,0 +1,52 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Extras.Runtime; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3SystemTextJson : IMessage, NullCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3SystemTextJson; + public SystemTextJsonSerializer GetSerializer() => new(); + public NullCompressor? GetCompressor() => default; + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3SystemTextJson(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3SystemTextJson other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3SystemTextJson)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJsonSourceGenerated.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJsonSourceGenerated.cs new file mode 100644 index 0000000..bf504f7 --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJsonSourceGenerated.cs @@ -0,0 +1,61 @@ +namespace Reloaded.Messaging.Tests.Messages; + +#if NET6_0_OR_GREATER +using Reloaded.Messaging.Interfaces; +using System; +using System.Text.Json; +using Reloaded.Messaging.Interfaces.Utilities; +using System.Text.Json.Serialization; +using Reloaded.Messaging.Extras.Runtime; +[JsonSourceGenerationOptionsAttribute(GenerationMode = JsonSourceGenerationMode.Default)] +[JsonSerializable(typeof(Vector3SystemTextJsonSourceGenerated))] +internal partial class Vector3JsonContext : JsonSerializerContext +{ +} + +public struct Vector3SystemTextJsonSourceGenerated : IMessage, NullCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3SystemTextJson; + public SourceGeneratedSystemTextJsonSerializer GetSerializer() => new(Vector3JsonContext.Default.Vector3SystemTextJsonSourceGenerated); + public NullCompressor? GetCompressor() => default; + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3SystemTextJsonSourceGenerated(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3SystemTextJsonSourceGenerated other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3SystemTextJsonSourceGenerated)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} +#endif \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3ZStandard.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3ZStandard.cs new file mode 100644 index 0000000..b17131e --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3ZStandard.cs @@ -0,0 +1,52 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Compressor.ZStandard; +using Reloaded.Messaging.Serializer.ReloadedMemory; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3ZStandard : IMessage, ZStandardCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3ZStandard; + public UnmanagedReloadedMemorySerializer GetSerializer() => new(); + public ZStandardCompressor GetCompressor() => new(); + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3ZStandard(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3ZStandard other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3ZStandard)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj b/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj index 6641fe3..ee0eae0 100644 --- a/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj +++ b/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj @@ -1,9 +1,11 @@  - netcoreapp3.0;NET472 - + netcoreapp3.1;net6.0 + preview false + true + enable @@ -17,17 +19,20 @@ - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - diff --git a/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs b/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs deleted file mode 100644 index 1fd28e7..0000000 --- a/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Runtime.InteropServices; -using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.MessagePack; - -namespace Reloaded.Messaging.Tests.Struct -{ - public struct StringMessage : IMessage - { - public MessageType GetMessageType() => MessageType.String; - public ISerializer GetSerializer() => new MsgPackSerializer(true); - public ICompressor GetCompressor() => null; - - public string Text { get; set; } - - public StringMessage(string text) - { - Text = text; - } - - /* Auto Generated by R# */ - public bool Equals(StringMessage other) - { - return string.Equals(Text, other.Text); - } - - public override bool Equals(object obj) - { - return obj is StringMessage other && Equals(other); - } - - public override int GetHashCode() - { - return (Text != null ? Text.GetHashCode() : 0); - } - } -} diff --git a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs b/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs deleted file mode 100644 index 08a2f8d..0000000 --- a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.MessagePack; - -namespace Reloaded.Messaging.Tests.Struct -{ - public struct Vector3 : IMessage - { - public MessageType GetMessageType() => MessageType.Vector3; - public ISerializer GetSerializer() => new MsgPackSerializer(true); - public ICompressor GetCompressor() => null; - - public float X { get; set; } - public float Y { get; set; } - public float Z { get; set; } - - public Vector3(float x, float y, float z) - { - X = x; - Y = y; - Z = z; - } - - /* Auto-implemented by R# */ - private bool Equals(Vector3 other) - { - return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - return false; - - if (ReferenceEquals(this, obj)) - return true; - - if (obj.GetType() != this.GetType()) - return false; - - return Equals((Vector3)obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = X.GetHashCode(); - hashCode = (hashCode * 397) ^ Y.GetHashCode(); - hashCode = (hashCode * 397) ^ Z.GetHashCode(); - return hashCode; - } - } - - } -} diff --git a/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs deleted file mode 100644 index 4159ce0..0000000 --- a/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Reloaded.Messaging.Compressor.ZStandard; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.ReloadedMemory; -using Reloaded.Messaging.Tests.Struct; -using Xunit; -using ZstdNet; - -namespace Reloaded.Messaging.Tests.Tests.Compression -{ - public class CompressionTest : IDisposable - { - public void Dispose() - { - Overrides.SerializerOverride.Remove(typeof(Vector3)); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - } - - [Fact] - public void CheckCompression() - { - var originalVector = new Vector3(235F, 10F, 5F); - var vectorMessage = new Message(originalVector); - - // Set serialization: Reloaded. - // Set compression: None - GC.Collect(); - Overrides.SerializerOverride[typeof(Vector3)] = new ReloadedMemorySerializer(false); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - - var serializedUncompressed = vectorMessage.Serialize(); - var deserializedUncompressed = Message.Deserialize(serializedUncompressed); - Assert.Equal(originalVector, deserializedUncompressed); - - // Set compression: ZStandard - Overrides.CompressorOverride[typeof(Vector3)] = new ZStandardCompressor(new CompressionOptions(22)); - var serializedZStandard = vectorMessage.Serialize(); - var deserializedZStandard = Message.Deserialize(serializedZStandard); - - Assert.Equal(originalVector, deserializedZStandard); - Assert.NotEqual(serializedUncompressed, serializedZStandard); - - // Note: Compression here acts negatively. - } - } -} diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs deleted file mode 100644 index 80def5e..0000000 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Tests.Struct; -using Xunit; - -namespace Reloaded.Messaging.Tests.Tests.Serialization -{ - public class PureSerialize - { - - /* Tests Pure Serialization: No Message Passing involved. */ - - [Fact] - public void SerializeAndDeserialize() - { - var vector = new Vector3(0, 25, 100); - byte[] data = vector.Serialize(); - var newVector = Serializable.Deserialize(data); - Assert.Equal(vector, newVector); - } - } -} diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs deleted file mode 100644 index c3e08e8..0000000 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading; -using LiteNetLib; -using Newtonsoft.Json; -using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.MessagePack; -using Reloaded.Messaging.Serializer.NewtonsoftJson; -using Reloaded.Messaging.Serializer.ReloadedMemory; -using Reloaded.Messaging.Serializer.SystemTextJson; -using Reloaded.Messaging.Structs; -using Reloaded.Messaging.Tests.Init; -using Reloaded.Messaging.Tests.Struct; -using Xunit; - -namespace Reloaded.Messaging.Tests.Tests.Serialization -{ - public class StringPassTest : IDisposable - { - private const string Message = "Test Message"; - private TestingHosts _hosts; - - public StringPassTest() - { - _hosts = new TestingHosts(); - } - - public void Dispose() - { - Overrides.SerializerOverride.Remove(typeof(StringMessage)); - Overrides.CompressorOverride.Remove(typeof(StringMessage)); - _hosts.Dispose(); - } - - [Fact(Timeout = 1000)] - public void MsgPackPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new MsgPackSerializer(false); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } - - [Fact(Timeout = 1000)] - public void ReloadedPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new ReloadedMemorySerializer(true); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } - - [Fact(Timeout = 1000)] - public void SystemTextJsonPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new SystemTextJsonSerializer(new JsonSerializerOptions()); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } - - [Fact(Timeout = 1000)] - public void NewtonsoftPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } - - private void PassString() - { - string delivered = default; - - // Message handling method - void Handler(ref NetMessage netMessage) - { - delivered = netMessage.Message.Text; - } - - // Setup client. - _hosts.SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); - - // Send Message. - var stringMessage = new Message(new StringMessage(Message)); - var data = stringMessage.Serialize(); - _hosts.SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); - - while (delivered == default) - Thread.Sleep(16); - - Assert.Equal(Message, delivered); - } - } -} diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs deleted file mode 100644 index 6b240bc..0000000 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading; -using LiteNetLib; -using Newtonsoft.Json; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.MessagePack; -using Reloaded.Messaging.Serializer.NewtonsoftJson; -using Reloaded.Messaging.Serializer.ReloadedMemory; -using Reloaded.Messaging.Serializer.SystemTextJson; -using Reloaded.Messaging.Structs; -using Reloaded.Messaging.Tests.Init; -using Xunit; -using Vector3 = Reloaded.Messaging.Tests.Struct.Vector3; - -namespace Reloaded.Messaging.Tests.Tests.Serialization -{ - public class VectorPassTest : IDisposable - { - private TestingHosts _hosts; - - public VectorPassTest() - { - _hosts = new TestingHosts(); - } - - public void Dispose() - { - Overrides.SerializerOverride.Remove(typeof(Vector3)); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - _hosts.Dispose(); - } - - [Fact(Timeout = 1000)] - public void MsgPackPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new MsgPackSerializer(false); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } - - [Fact(Timeout = 1000)] - public void ReloadedPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new ReloadedMemorySerializer(false); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } - - [Fact(Timeout = 1000)] - public void SystemTextJsonPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new SystemTextJsonSerializer(new JsonSerializerOptions()); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } - - [Fact(Timeout = 1000)] - public void NewtonsoftPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } - - private void PassVector3() - { - Vector3 message = new Vector3(1.0F, 235.0F, 100.0F); - Vector3 delivered = default; - - // Message handling method - void Handler(ref NetMessage netMessage) - { - delivered = netMessage.Message; - } - - // Setup client - _hosts.SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); - - // Send Message. - var vectorMessage = new Message(message); - var data = vectorMessage.Serialize(); - _hosts.SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); - - while (delivered.Equals(default(Vector3))) - Thread.Sleep(16); - - Assert.Equal(message, delivered); - } - } -} diff --git a/Source/Reloaded.Messaging.Trimming/Program.cs b/Source/Reloaded.Messaging.Trimming/Program.cs new file mode 100644 index 0000000..3751555 --- /dev/null +++ b/Source/Reloaded.Messaging.Trimming/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/Source/Reloaded.Messaging.Trimming/Reloaded.Messaging.Trimming.csproj b/Source/Reloaded.Messaging.Trimming/Reloaded.Messaging.Trimming.csproj new file mode 100644 index 0000000..18b39e0 --- /dev/null +++ b/Source/Reloaded.Messaging.Trimming/Reloaded.Messaging.Trimming.csproj @@ -0,0 +1,32 @@ + + + + Exe + net6.0 + enable + enable + + + true + link + false + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Reloaded.Messaging.sln b/Source/Reloaded.Messaging.sln index 745024d..bc6f361 100644 --- a/Source/Reloaded.Messaging.sln +++ b/Source/Reloaded.Messaging.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29006.145 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32611.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging", "Reloaded.Messaging\Reloaded.Messaging.csproj", "{0F5EE206-32FC-404F-80FB-E02B37AE2855}" EndProject @@ -15,9 +15,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Compress EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Interfaces", "Reloaded.Messaging.Interfaces\Reloaded.Messaging.Interfaces.csproj", "{90FF55E7-AFAC-4089-BAAD-B9BFF51E2447}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Serializer.SystemTextJson", "Reloaded.Messaging.Serializer.SystemTextJson\Reloaded.Messaging.Serializer.SystemTextJson.csproj", "{E80F0107-A3A7-4DDF-BA5F-3C771F54062E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Extras.Runtime", "Reloaded.Messaging.Extras.Runtime\Reloaded.Messaging.Extras.Runtime.csproj", "{E80F0107-A3A7-4DDF-BA5F-3C771F54062E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Serializer.NewtonsoftJson", "Reloaded.Messaging.Serializer.NewtonsoftJson\Reloaded.Messaging.Serializer.NewtonsoftJson.csproj", "{62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Host.LiteNetLib", "Reloaded.Messaging.Host.LiteNetLib\Reloaded.Messaging.Host.LiteNetLib.csproj", "{1189FFF2-616A-478D-99C7-C1FDC068413C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extra", "Extra", "{73B9A805-15CC-4DE4-8039-7FEBB366D4E1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Benchmarks", "Reloaded.Messaging.Benchmarks\Reloaded.Messaging.Benchmarks.csproj", "{05BAA584-FEFF-4180-8035-6F075A17A051}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Trimming", "Reloaded.Messaging.Trimming\Reloaded.Messaging.Trimming.csproj", "{A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -53,14 +59,26 @@ Global {E80F0107-A3A7-4DDF-BA5F-3C771F54062E}.Debug|Any CPU.Build.0 = Debug|Any CPU {E80F0107-A3A7-4DDF-BA5F-3C771F54062E}.Release|Any CPU.ActiveCfg = Release|Any CPU {E80F0107-A3A7-4DDF-BA5F-3C771F54062E}.Release|Any CPU.Build.0 = Release|Any CPU - {62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}.Release|Any CPU.Build.0 = Release|Any CPU + {1189FFF2-616A-478D-99C7-C1FDC068413C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1189FFF2-616A-478D-99C7-C1FDC068413C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1189FFF2-616A-478D-99C7-C1FDC068413C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1189FFF2-616A-478D-99C7-C1FDC068413C}.Release|Any CPU.Build.0 = Release|Any CPU + {05BAA584-FEFF-4180-8035-6F075A17A051}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05BAA584-FEFF-4180-8035-6F075A17A051}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05BAA584-FEFF-4180-8035-6F075A17A051}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05BAA584-FEFF-4180-8035-6F075A17A051}.Release|Any CPU.Build.0 = Release|Any CPU + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {05BAA584-FEFF-4180-8035-6F075A17A051} = {73B9A805-15CC-4DE4-8039-7FEBB366D4E1} + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7} = {73B9A805-15CC-4DE4-8039-7FEBB366D4E1} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E6F63326-4EEA-4D4E-BB44-60FD2065CB3E} EndGlobalSection diff --git a/Source/Reloaded.Messaging/MessageDispatcher.cs b/Source/Reloaded.Messaging/MessageDispatcher.cs new file mode 100644 index 0000000..f3431df --- /dev/null +++ b/Source/Reloaded.Messaging/MessageDispatcher.cs @@ -0,0 +1,67 @@ +using System; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Utilities; + +#if NET5_0_OR_GREATER +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#endif + +namespace Reloaded.Messaging; + +/// +/// Provides a generic mechanism for dispatching messages to registered handlers. +/// +/// Type of parameter. +public struct MessageDispatcher : IMessageDispatcher +{ + // Note: We allocate 255 handlers to remove a branch in `GetHandlerForType` in cases where bad actor may pass invalid value. + // bit wasteful memory wise but it is what it is. + + private IMessageHandlerBase?[] _handlers; + + /// + public MessageDispatcher() => _handlers = new IMessageHandlerBase[byte.MaxValue]; + + /// + /// Gets a handler that handles a specific message type. + /// + /// The type of message requested. + /// Handler for the specific message. Might be null. + public ref IMessageHandlerBase? GetHandlerForType(byte messageType) + { +#if NET5_0_OR_GREATER + ref var writer = ref MemoryMarshal.GetArrayDataReference(_handlers); + return ref Unsafe.Add(ref writer, (int)messageType); +#else + return ref _handlers[messageType]; +#endif + } + + /// + /// Sets a handler for a specific message type. + /// + public void AddOrOverrideHandler(IMessageHandlerBase handler) => _handlers[handler.GetMessageType()] = handler; + + /// + /// Removes a handler assigned to a specific message type. + /// + public void RemoveHandler(byte messageType) => _handlers[messageType] = null; + + /// + /// Given a raw network message, decodes the message and delegates it to an appropriate handling method. + /// + /// Data containing a packed Reloaded.Messaging message. + /// The extra data associated with this request. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public void Dispatch(Span data, ref TExtraData extraData) + { + HeaderReader.ReadHeader(data, out var messageType, out var sizeAfterCompression, out var headerSize); + ref var handler = ref GetHandlerForType((byte)messageType); + handler?.HandleMessage(SpanExtensions.SliceFast(data, headerSize), sizeAfterCompression, ref extraData); + } +} + diff --git a/Source/Reloaded.Messaging/MessageHandler.cs b/Source/Reloaded.Messaging/MessageHandler.cs deleted file mode 100644 index 2f342d9..0000000 --- a/Source/Reloaded.Messaging/MessageHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; -using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Structs; - -namespace Reloaded.Messaging -{ - /// - /// Provides a generic mechanism for dispatching messages received from a client or server. - /// Works by assigning functions to specified message "types", declared by . - /// - /// Type of value to map to individual message handlers. - public class MessageHandler where TMessageType : unmanaged - { - private Dictionary _mapping; - - public MessageHandler() - { - _mapping = new Dictionary(); - } - - /// - /// Given a raw network message, decodes the message and delegates it to an appropriate handling method. - /// - public void Handle(ref RawNetMessage parameters) - { - var messageType = MessageBase.GetMessageType(parameters.Message); - if (_mapping.TryGetValue(messageType, out RawNetMessageHandler value)) - { - value(ref parameters); - } - } - - /// - /// Sets a method to execute handling a specific - /// - public void AddOrOverrideHandler(Handler handler) where TStruct : IMessage, new() - { - var messageType = MessageExtensions.GetMessageType(new TStruct()); - AddOrOverrideHandler(messageType, handler); - } - - /// - /// Sets a method to execute handling a specific - /// - public void AddOrOverrideHandler(TMessageType messageType, Handler handler) where TStruct : IMessage, new() - { - RawNetMessageHandler parameters = delegate (ref RawNetMessage rawMessage) - { - var message = Message.Deserialize(rawMessage.Message); - var netMessage = new NetMessage(ref message, ref rawMessage); - handler(ref netMessage); - }; - - _mapping[messageType] = parameters; - } - - /// - /// Removes the current method assigned to a handle a message of a specific - /// - public void RemoveHandler(TMessageType messageType) - { - _mapping.Remove(messageType); - } - - public delegate void Handler(ref NetMessage netMessage); - private delegate void RawNetMessageHandler(ref RawNetMessage rawNetMessage); - } -} diff --git a/Source/Reloaded.Messaging/Messages/Disposables/ReusableSingletonMemoryStream.cs b/Source/Reloaded.Messaging/Messages/Disposables/ReusableSingletonMemoryStream.cs new file mode 100644 index 0000000..de94f71 --- /dev/null +++ b/Source/Reloaded.Messaging/Messages/Disposables/ReusableSingletonMemoryStream.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.IO; +using Reloaded.Messaging.Utilities; + +namespace Reloaded.Messaging.Messages.Disposables; + +/// +/// Wrapper of a recyclable memory stream exposing the raw data underneath. +/// +public struct ReusableSingletonMemoryStream : IDisposable +{ + internal RecyclableMemoryStream Stream; + + /// + /// Provides access to the underlying span. + /// + public Span Span => SpanExtensions.AsSpanFast(Stream.GetBuffer(), (int)Stream.Length); + + /// + /// The stream in question. + public ReusableSingletonMemoryStream(RecyclableMemoryStream stream) { Stream = stream; } + + /// + /// Disposes the underlying stream. + /// + public void Dispose() => Stream.SetLength(0); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/HeaderReader.cs b/Source/Reloaded.Messaging/Messages/HeaderReader.cs new file mode 100644 index 0000000..f1db778 --- /dev/null +++ b/Source/Reloaded.Messaging/Messages/HeaderReader.cs @@ -0,0 +1,59 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Reloaded.Messaging.Messages; + +/// +/// Reads the header of a packed message. +/// +public struct HeaderReader +{ + /// + /// Size of standard header, without compression. + /// + public const int StandardHeaderSize = 1; + + /// + /// Size of standard header, with compression. + /// + public const int CompressedHeaderSize = 5; + + /// + public const byte CompressionFlag = 0b10000000; + + + /// + public const int HeaderDecompressedDataSize = sizeof(uint); + + /// + /// Reads the message header of a packaged message. + /// + /// Span containing the header at the start. + /// The type of message in this header. + /// The size of the compressed payload. This is -1 if there is no compression. + /// Size of the header at the start of the span. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadHeader(Span data, out sbyte messageType, out int sizeAfterDecompression, out int headerSize) + { + messageType = (sbyte)MemoryMarshal.GetReference(data); + + if ((messageType & CompressionFlag) == CompressionFlag) + { + sizeAfterDecompression = Unsafe.AsRef(Unsafe.Add(ref MemoryMarshal.GetReference(data), 1)); + messageType = (sbyte)(messageType ^ CompressionFlag); + if (!BitConverter.IsLittleEndian) // Evaluated at JIT time. + sizeAfterDecompression = BinaryPrimitives.ReverseEndianness(sizeAfterDecompression); + + headerSize = CompressedHeaderSize; + return; + } + + headerSize = StandardHeaderSize; + sizeAfterDecompression = -1; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs b/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs deleted file mode 100644 index 6ae51ca..0000000 --- a/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Messages -{ - public static class MessageExtensions - { - public static TMessageType GetMessageType(this IMessage message) where TMessageType : unmanaged - { - return message.GetMessageType(); - } - } -} diff --git a/Source/Reloaded.Messaging/Messages/Message.cs b/Source/Reloaded.Messaging/Messages/Message.cs deleted file mode 100644 index b8d3a68..0000000 --- a/Source/Reloaded.Messaging/Messages/Message.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Messages -{ - public unsafe class Message : MessageBase where TStruct : IMessage, new() where TMessageType : unmanaged - { - // ReSharper disable StaticMemberInGenericType - private static ISerializer DefaultSerializer { get; set; } - private static ICompressor DefaultCompressor { get; set; } - private static bool DefaultCompressorSet { get; set; } - // ReSharper restore StaticMemberInGenericType - - public TStruct ActualMessage { get; set; } - - public Message(TStruct message) - { - ActualMessage = message; - } - - /// - /// Serializes the current instance and returns an array of bytes representing the instance. - /// - public byte[] Serialize() - { - // Perform serialization. - var serializer = GetSerializer(); - var message = ActualMessage; - var encodedMessage = serializer.Serialize(ref message); - - // Allocate memory for result and write header. - var result = new byte[encodedMessage.Length + sizeof(TMessageType)]; - var resultSpan = result.AsSpan(); - var messageType = ActualMessage.GetMessageType(); - -#if (USE_NATIVE_SPAN_API) - var readOnlyMessageType = MemoryMarshal.CreateReadOnlySpan(ref messageType, sizeof(TMessageType)); - var readOnlyMessageTypeBytes = MemoryMarshal.AsBytes(readOnlyMessageType); - readOnlyMessageTypeBytes.CopyTo(resultSpan); -#else - byte* bytes = (byte*)Unsafe.AsPointer(ref messageType); - var readOnlyMessageTypeBytes = new Span(bytes, sizeof(TMessageType)); - readOnlyMessageTypeBytes.CopyTo(resultSpan); -#endif - - // Append serialized data. - resultSpan = resultSpan.Slice(sizeof(TMessageType)); - encodedMessage.AsSpan().CopyTo(resultSpan); - - var compressor = GetCompressor(); - if (compressor != null) - result = compressor.Compress(result); - - return result; - } - - /// - /// Deserializes a given set of bytes into a usable struct. - /// - public static TStruct Deserialize(byte[] serializedBytes) - { - // Get decompressor. - var compressor = GetCompressor(); - if (compressor != null) - serializedBytes = compressor.Decompress(serializedBytes); - - // Get serializer - var serializer = GetSerializer(); - - // Read messagepack message. - var messageSegment = serializedBytes.AsSpan(sizeof(TMessageType)).ToArray(); - var message = serializer.Deserialize(messageSegment); - - return message; - - // Note: No need to read MessageType. MessageType was only necessary to link a message to correct handler. - } - - private static ISerializer GetSerializer() - { - if (Overrides.SerializerOverride.TryGetValue(typeof(TStruct), out ISerializer value)) - return value; - - if (DefaultSerializer == null) - { - var defaultStruct = new TStruct(); - DefaultSerializer = ((IMessage)defaultStruct).GetSerializer(); - } - - return DefaultSerializer; - } - - private static ICompressor GetCompressor() - { - if (Overrides.CompressorOverride.TryGetValue(typeof(TStruct), out ICompressor value)) - return value; - - if (! DefaultCompressorSet) - { - var defaultStruct = new TStruct(); - DefaultCompressor = ((IMessage)defaultStruct).GetCompressor(); - DefaultCompressorSet = true; - } - - return DefaultCompressor; - } - } -} diff --git a/Source/Reloaded.Messaging/Messages/MessageBase.cs b/Source/Reloaded.Messaging/Messages/MessageBase.cs deleted file mode 100644 index 72f00e2..0000000 --- a/Source/Reloaded.Messaging/Messages/MessageBase.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Reloaded.Messaging.Messages -{ - public unsafe class MessageBase where TMessageType : unmanaged - { - public static TMessageType GetMessageType(byte[] serializedBytes) - { - fixed (byte* arrayPtr = serializedBytes) - { - return *(TMessageType*) arrayPtr; - } - } - } -} diff --git a/Source/Reloaded.Messaging/Messages/MessageReader.cs b/Source/Reloaded.Messaging/Messages/MessageReader.cs new file mode 100644 index 0000000..960ba5b --- /dev/null +++ b/Source/Reloaded.Messaging/Messages/MessageReader.cs @@ -0,0 +1,51 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Utilities; + +namespace Reloaded.Messaging.Messages; + +/// +/// Struct used for deserializing of messages. +/// +/// The structure represented by the message. +/// Type of serializer used. +/// Type of compressor used. +public unsafe struct MessageReader where TStruct : IMessage + where TSerializer : ISerializer + where TCompressor : ICompressor +{ + private TCompressor? _compressor; + private TSerializer _serializer; + + /// + /// Deserializes messages with specified type. + /// + /// A sample structure to extract compressor and serializer from. + public MessageReader(in TStruct sample) + { + _compressor = sample.GetCompressor(); + _serializer = sample.GetSerializer(); + } + + /// + /// Deserializes a given set of bytes into a usable struct. + /// + /// The raw bytes containing the message, without message header. + /// Expected size after decompression, ignored if no compression will be used. + public TStruct Deserialize(Span serializedBytes, int decompressedSize) + { + // Get decompressor. + if (_compressor != null) + { + // Decompress + using var decompressedRental = new ArrayRental(decompressedSize); + var decompressedSpan = decompressedRental.Span; + _compressor.Decompress(serializedBytes, decompressedSpan); + + // Deserialize. + return _serializer.Deserialize(decompressedSpan); + } + + return _serializer.Deserialize(serializedBytes); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/MessageWriter.cs b/Source/Reloaded.Messaging/Messages/MessageWriter.cs new file mode 100644 index 0000000..12d64ff --- /dev/null +++ b/Source/Reloaded.Messaging/Messages/MessageWriter.cs @@ -0,0 +1,121 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Runtime.CompilerServices; +using Microsoft.IO; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Interfaces.Message; +using Reloaded.Messaging.Messages.Disposables; +using Reloaded.Messaging.Utilities; + +namespace Reloaded.Messaging.Messages; + +/// +/// Writes single messages to be sent via the network. +/// +/// The structure represented by the message. +/// Structure used to perform serialization. +/// Structure used to perform compression. +public static unsafe class MessageWriter where TStruct : IMessage + where TSerializer : ISerializer + where TCompressor : ICompressor +{ + /// + /// Serializes the current instance and returns a disposable memory array of data representing the instance. + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReusableSingletonMemoryStream Serialize(ref TStruct data) + { + // Get the compressor. + // If there is no compressor, we can write data type directly and then ser + var compressor = data.GetCompressor(); + if (compressor != null) + return SerializeToRecyclableMemoryStream(ref data, Pool.MessageStreamPerThread(), compressor); + else + return SerializeToRecyclableMemoryStream(ref data, Pool.MessageStreamPerThread()); + } + + /// + /// Serializes the current structure to, a provided with compression.

+ /// Internal-ish API intended for benchmarking. + ///
+ /// The data to serialize. + /// The memory stream to serialize to. + /// The compressor to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReusableSingletonMemoryStream SerializeToRecyclableMemoryStream(ref TStruct data, RecyclableMemoryStream messageStream, TCompressor compressor) + { + // Serialize message first. + var serializer = data.GetSerializer(); + using var serializeStream = new ReusableSingletonMemoryStream(Pool.CompressionStreamPerThread()); + serializer.Serialize(ref data, serializeStream.Stream); + var serializedSpan = serializeStream.Span; + var decompressedSize = serializedSpan.Length; + + // Write data type. + var messageTypePacked = data.GetMessageType() | HeaderReader.CompressionFlag; + messageStream.WriteByte((byte)messageTypePacked); + + // Reserve space for compressed data. + var compressedLengthPos = messageStream.Position; + messageStream.Seek(HeaderReader.HeaderDecompressedDataSize, SeekOrigin.Current); + var compressedDataStartPos = messageStream.Position; + + // Compress directly into stream. + var maxCompressedSize = compressor.GetMaxCompressedSize((int)serializeStream.Stream.Length); + var compressedSpan = messageStream.GetSpan(maxCompressedSize); + int numCompressed = compressor.Compress(serializedSpan, compressedSpan); + + // Write compressed size + messageStream.Seek(compressedLengthPos, SeekOrigin.Begin); + if (!BitConverter.IsLittleEndian) // evaluated at JIT time, it's an intrinsic. + decompressedSize = BinaryPrimitives.ReverseEndianness(decompressedSize); + + messageStream.Write(SpanExtensions.AsByteSpanFast(&decompressedSize)); + + // Set length (to account for compressed # bytes). + messageStream.SetLength(compressedDataStartPos + numCompressed); + return new ReusableSingletonMemoryStream(messageStream); + } + + /// + /// Serializes the current structure to, a provided without using compression.

+ /// Internal-ish API intended for benchmarking. + ///
+ /// The data to serialize. + /// The memory stream to serialize to. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReusableSingletonMemoryStream SerializeToRecyclableMemoryStream(ref TStruct data, RecyclableMemoryStream messageStream) + { + // If there is no compression, we can write type then data directly. + messageStream.WriteByte((byte)data.GetMessageType()); + data.GetSerializer().Serialize(ref data, messageStream); + return new ReusableSingletonMemoryStream(messageStream); + } +} + +/// +/// Extension methods for easier serialization and deserialization. +/// +public static unsafe class MessageWriterExtensions +{ + // Note: It's a mess but has no impact on JIT-ted code. + + /// + /// Serializes the current instance and returns a disposable memory array of data representing the instance. + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReusableSingletonMemoryStream Serialize(this IMessage dummy0, ref TStruct data, TSerializer? dummy1 = default, TCompressor? dummy2 = default) + where TStruct : IMessage + where TSerializer : ISerializer + where TCompressor : ICompressor + { + return MessageWriter.Serialize(ref data); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Overrides.cs b/Source/Reloaded.Messaging/Overrides.cs deleted file mode 100644 index 4953eea..0000000 --- a/Source/Reloaded.Messaging/Overrides.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging -{ - /// - /// Class which provides, client-side the ability to override serializers and compressors for specified types. - /// Use either for testing or benchmarking. - /// - public static class Overrides - { - public static Dictionary SerializerOverride { get; } = new Dictionary(); - public static Dictionary CompressorOverride { get; } = new Dictionary(); - } -} diff --git a/Source/Reloaded.Messaging/ReferenceMessageHandler.cs b/Source/Reloaded.Messaging/ReferenceMessageHandler.cs new file mode 100644 index 0000000..fefcaa2 --- /dev/null +++ b/Source/Reloaded.Messaging/ReferenceMessageHandler.cs @@ -0,0 +1,70 @@ +using System; +using System.Runtime.CompilerServices; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Messages; + +namespace Reloaded.Messaging; + +/// +/// The base class for message handlers. +/// +public class ReferenceMessageHandler : IMessageHandlerBase + where TStruct : IMessage + where TCompressor : ICompressor + where TSerializer : ISerializer + where TMsgRefAction : IMsgRefAction +{ + private MessageReader _deserializer; + private TMsgRefAction _refAction = default!; + private sbyte _messageType; + + /// + /// No default constructor. + /// + private ReferenceMessageHandler() { } + + /// + /// Sample structure to extract message deserializer from. + /// + /// Sample structure + /// Action to perform for each received message. + public ReferenceMessageHandler(in TStruct sample, TMsgRefAction action) + { + _deserializer = new MessageReader(sample); + _messageType = sample.GetMessageType(); + _refAction = action; + } + + /// + public sbyte GetMessageType() => _messageType; + + /// + public void HandleMessage(Span data, int decompressedSize, ref TExtraData extraData) + { + var deserialized = _deserializer.Deserialize(data, decompressedSize); + _refAction.OnMessageReceive(ref deserialized, ref extraData); + } + + /// + /// Deserializes the message from a raw array of bytes. + /// + /// The data to deserialize. + /// Decompressed size of data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TStruct Deserialize(Span data, int decompressedSize) => _deserializer.Deserialize(data, decompressedSize); +} + +/// +/// Provides a function to execute for the . +/// +/// The structure of received message. +/// Extra data held by the message. +public interface IMsgRefAction +{ + /// + /// Runs the code associated with the reference action. + /// + /// + /// + void OnMessageReceive(ref TStruct received, ref TExtraData data); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj index 885375b..3f066a9 100644 --- a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj +++ b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj @@ -1,38 +1,34 @@  - netstandard2.1;netstandard2.0;netcoreapp3.1 - false + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 + preview Sewer56 - Reloaded (Mod Loader) II's extensible "event-like" solution for passing messages across a local or remote network that sits ontop of LiteNetLib. + Reloaded II's high performance solution for adding high performance, near zero-overhead message packing to existing libraries. LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true Sewer56 - 1.3.0 + 2.0.0 true - $(DefineConstants);USE_NATIVE_SPAN_API - $(DefineConstants);USE_NATIVE_UNSAFE - - - + $(DefineConstants);USE_NATIVE_SPAN_API + $(DefineConstants);USE_NATIVE_UNSAFE + 1701;1702;NU5104 true - obj\\Reloaded.Messaging.xml - + NuGet-Icon.png + enable - - true - obj\\Reloaded.Messaging.xml + false + true - - - + + + @@ -40,6 +36,10 @@ True + + True + \ + diff --git a/Source/Reloaded.Messaging/SimpleHost.cs b/Source/Reloaded.Messaging/SimpleHost.cs deleted file mode 100644 index 7ef8790..0000000 --- a/Source/Reloaded.Messaging/SimpleHost.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using LiteNetLib; -using Reloaded.Messaging.Structs; - -namespace Reloaded.Messaging -{ - /// - /// Provides a simple client or host based off of LiteNetLib. - /// - public class SimpleHost : IDisposable where TMessageType : unmanaged - { - /// - /// The password necessary to join this host. If it does not match, incoming clients will be rejected. - /// - public string Password { get; set; } - - /// - /// Set to true to accept incoming clients, else reject all clients. - /// - public bool AcceptClients { get; set; } - - /// - /// Event for handling connection requests. - /// - public event EventBasedNetListener.OnConnectionRequest ConnectionRequestEvent; - - /// - /// Dispatcher for individual (s) to your events. - /// - public MessageHandler MessageHandler { get; private set; } - - /// - public EventBasedNetListener Listener { get; private set; } - - /// - public NetManager NetManager { get; private set; } - - - public SimpleHost(bool acceptClients, string password = "") - { - Password = password; - AcceptClients = acceptClients; - MessageHandler = new MessageHandler(); - Listener = new EventBasedNetListener(); - Listener.NetworkReceiveEvent += OnNetworkReceive; - Listener.ConnectionRequestEvent += ListenerOnConnectionRequestEvent; - - NetManager = new NetManager(Listener); - NetManager.UnsyncedEvents = true; - NetManager.AutoRecycle = true; - } - - /// - public void Dispose() - { - NetManager.Stop(); - } - - private void ListenerOnConnectionRequestEvent(ConnectionRequest request) - { - if (ConnectionRequestEvent != null) - { - ConnectionRequestEvent(request); - return; - } - - if (AcceptClients) - request.AcceptIfKey(Password); - else - request.Reject(); - } - - /* On each message received. */ - private void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliverymethod) - { - byte[] rawBytes = reader.GetRemainingBytes(); - var rawNetMessage = new RawNetMessage(rawBytes, peer, reader, deliverymethod); - MessageHandler.Handle(ref rawNetMessage); - } - } -} diff --git a/Source/Reloaded.Messaging/Structs/NetMessage.cs b/Source/Reloaded.Messaging/Structs/NetMessage.cs deleted file mode 100644 index b08fa71..0000000 --- a/Source/Reloaded.Messaging/Structs/NetMessage.cs +++ /dev/null @@ -1,20 +0,0 @@ -using LiteNetLib; - -namespace Reloaded.Messaging.Structs -{ - public struct NetMessage - { - public TStruct Message { get; private set; } - public NetPeer Peer { get; private set; } - public NetPacketReader PacketReader { get; private set; } - public DeliveryMethod DeliveryMethod { get; private set; } - - public NetMessage(ref TStruct message, ref RawNetMessage rawMessage) - { - Message = message; - Peer = rawMessage.Peer; - PacketReader = rawMessage.PacketReader; - DeliveryMethod = rawMessage.DeliveryMethod; - } - } -} diff --git a/Source/Reloaded.Messaging/Structs/RawNetMessage.cs b/Source/Reloaded.Messaging/Structs/RawNetMessage.cs deleted file mode 100644 index 0f6bc49..0000000 --- a/Source/Reloaded.Messaging/Structs/RawNetMessage.cs +++ /dev/null @@ -1,20 +0,0 @@ -using LiteNetLib; - -namespace Reloaded.Messaging.Structs -{ - public struct RawNetMessage - { - public byte[] Message { get; private set; } - public NetPeer Peer { get; private set; } - public NetPacketReader PacketReader { get; private set; } - public DeliveryMethod DeliveryMethod { get; private set; } - - public RawNetMessage(byte[] message, NetPeer peer, NetPacketReader packetReader, DeliveryMethod deliveryMethod) - { - Message = message; - Peer = peer; - PacketReader = packetReader; - DeliveryMethod = deliveryMethod; - } - } -} diff --git a/Source/Reloaded.Messaging/Utilities/ArrayRental.cs b/Source/Reloaded.Messaging/Utilities/ArrayRental.cs new file mode 100644 index 0000000..4ffa547 --- /dev/null +++ b/Source/Reloaded.Messaging/Utilities/ArrayRental.cs @@ -0,0 +1,65 @@ +using System; +using System.Buffers; +#if NET5_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace Reloaded.Messaging.Utilities; + +/// +/// Represents a temporary array rental from the runtime's ArrayPool. +/// +/// Type of element to be rented from the runtime. +public struct ArrayRental : IDisposable +{ + private T[] _data; + private int _count; + + /// + /// Rents an array of bytes from the arraypool. + /// + /// Needed amount of bytes. + public ArrayRental(int count) + { + _data = ArrayPool.Shared.Rent(count); + _count = count; + } + + /// + /// Exposes the raw underlying array, which will likely + /// be bigger than the number of elements. + /// + public T[] RawArray => _data; + + /// + /// Returns the rented array as a span. + /// + public Span Span => SpanExtensions.AsSpanFast(_data, _count); + + /// + /// Exposes the number of elements stored by this structure. + /// + public int Count => _count; + + /// + /// Returns a reference to the first element. + /// + public ref T FirstElement => ref GetFirstElement(); + + /// + /// Returns the array to the pool. + /// + public void Dispose() => ArrayPool.Shared.Return(_data, false); + + /// + /// Returns a reference to the first element. + /// + private ref T GetFirstElement() + { +#if NET5_0 + return ref MemoryMarshal.GetArrayDataReference(_data); +#else + return ref _data[0]; +#endif + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Utilities/IMessageExtensions.cs b/Source/Reloaded.Messaging/Utilities/IMessageExtensions.cs new file mode 100644 index 0000000..0800a64 --- /dev/null +++ b/Source/Reloaded.Messaging/Utilities/IMessageExtensions.cs @@ -0,0 +1,61 @@ +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Utilities; + +/// +/// Extensions for classes inheriting from +/// +public static class MessageExtensions +{ + /// + /// Creates a reference message handler from an existing message structure.

+ /// Example usage:
+ /// `sample.CreateMessageHandler(new Vector3BrotliReceiveAction(), 0);`
+ /// `sample.CreateMessageHandler(new Vector3BrotliReceiveAction(), default(TExtraData));`

+ /// Where `sample` inherits from IMessage. + ///
+ /// Type of structure used. + /// Type of serializer used. + /// Type of compressor used. + /// Extra data, usually received from network interface etc. + /// Action to add to the reference message handler. + /// The structure containing the message. + /// The action to execute for each message received. + /// Any valid instance of extra data. + /// Dummy parameter for generic type inference. + /// Dummy parameter for generic type inference. + public static ReferenceMessageHandler CreateMessageHandler(this IMessage structure, TMsgRefAction refAction, TExtraData extraDataSample, TSerializer? dummy2 = default, TCompressor? dummy3 = default) + where TSerializer : ISerializer + where TCompressor : ICompressor + where TStruct : IMessage + where TMsgRefAction : IMsgRefAction + { + return new ReferenceMessageHandler((TStruct)structure, refAction); + } + + /// + /// Creates a reference message handler from an existing message structure.

+ /// Example usage:
+ /// `sample.AddToDispatcher(new Vector3BrotliReceiveAction(), ref dispatcher);`

+ /// Where `sample` inherits from IMessage. + ///
+ /// Type of structure used. + /// Type of serializer used. + /// Type of compressor used. + /// Extra data, usually received from network interface etc. + /// Action to add to the reference message handler. + /// The structure containing the message. + /// The action to execute for each message received. + /// The dispatcher to add event handler to. + /// Dummy parameter for generic type inference. + /// Dummy parameter for generic type inference. + public static void AddToDispatcher(this IMessage structure, TMsgRefAction refAction, ref MessageDispatcher dispatcher, TSerializer? dummy2 = default, TCompressor? dummy3 = default) + where TSerializer : ISerializer + where TCompressor : ICompressor + where TStruct : IMessage + where TMsgRefAction : IMsgRefAction + { + var handler = new ReferenceMessageHandler((TStruct)structure, refAction); + dispatcher.AddOrOverrideHandler(handler); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Utilities/Pool.cs b/Source/Reloaded.Messaging/Utilities/Pool.cs new file mode 100644 index 0000000..f747808 --- /dev/null +++ b/Source/Reloaded.Messaging/Utilities/Pool.cs @@ -0,0 +1,39 @@ +using Microsoft.IO; +using System; + +namespace Reloaded.Messaging.Utilities; + +internal class Pool +{ + private static RecyclableMemoryStreamManager _recyclableMemoryStreamManager = new(); + + static Pool() => _recyclableMemoryStreamManager.AggressiveBufferReturn = true; + + [ThreadStatic] + private static RecyclableMemoryStream? _compressedMemoryStream; + + [ThreadStatic] + private static RecyclableMemoryStream? _messageMemoryStream; + + internal static RecyclableMemoryStream CompressionStreamPerThread() + { + if (_compressedMemoryStream == null) + { + _compressedMemoryStream = (RecyclableMemoryStream)_recyclableMemoryStreamManager.GetStream(); + return _compressedMemoryStream; + } + + return _compressedMemoryStream; + } + + internal static RecyclableMemoryStream MessageStreamPerThread() + { + if (_messageMemoryStream == null) + { + _messageMemoryStream = (RecyclableMemoryStream)_recyclableMemoryStreamManager.GetStream(); + return _messageMemoryStream; + } + + return _messageMemoryStream; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Utilities/SpanExtensions.cs b/Source/Reloaded.Messaging/Utilities/SpanExtensions.cs new file mode 100644 index 0000000..30d4c94 --- /dev/null +++ b/Source/Reloaded.Messaging/Utilities/SpanExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Runtime.CompilerServices; +#if NET5_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace Reloaded.Messaging.Utilities; + +/// +/// Extension methods related to spans. +/// +public static class SpanExtensions +{ + /// + /// Provides zero overhead unsafe array to span conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpanFast(T[] data, int length) + { +#if NET5_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(data), length); +#else + return data.AsSpan(0, length); +#endif + } + + /// + /// Converts a pointer to type T into a byte span with equivalent number of bytes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span AsByteSpanFast(T* data) where T : unmanaged + { +#if NET5_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref Unsafe.AsRef(data), sizeof(T)); +#else + return new Span(data, sizeof(T)); +#endif + } + + /// + /// Slices a span without any bounds checks. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span SliceFast(Span data, int start) + { +#if NET5_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetReference(data), start), data.Length - start); +#else + return data.Slice(start); +#endif + } +} \ No newline at end of file diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 0000000..0a9acc4 --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,77 @@ +# Sample Benchmarks + +Performed using Version 2.0.0 on the following configuration: + +``` +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1806 (21H2) +Intel Core i7-4790K CPU 4.00GHz (Haswell), 1 CPU, 8 logical and 4 physical cores +.NET SDK=7.0.100-preview.5.22307.18 + [Host] : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT +``` + +All benchmarks performed on single thread; albeit library is safe to use with multiple threads. + +## Message Unpacker & Dispatcher Benchmarks + +This is a benchmark of `MessageDispatcher`. + +This benchmark measures the time taken to: +- Decode Message Header (Unpack) +- Send Message to Handler for Deserialization + +``` +| Method | NumItems | Mean | Error | StdDev | Ratio | Operations/s | Allocated | +|-------------- |--------- |---------:|----------:|----------:|------:|-------------:|----------:| +| HandleMessage | 100000 | 1.038 ms | 0.0167 ms | 0.0148 ms | 1.00 | 96332329.33 | 2 B | +``` + +Around 96 million messages per second. + +## Message Creation Benchmarks + +This is a benchmark of `MessageWriter.Serialize`. + +This benchmark measures the time taken to: +- Create & Dispose container used for storing packed message. (`Microsoft.IO.RecyclableMemoryStream`) +- Create Message Header. +- Pack Message (combine header + raw data). + +``` +| Method | NumItems | Mean | Error | StdDev | Ratio | RatioSD | Operations/s | Allocated | +|----------------------------------- |--------- |-------------:|-----------:|-----------:|---------:|--------:|--------------:|----------:| +| DummySerializeOnly_BASELINE | 100000 | 13.85 us | 0.062 us | 0.058 us | 1.00 | 0.00 | 7222495640.56 | - | +| DummySerialize_And_Pack | 100000 | 7,294.25 us | 53.658 us | 47.567 us | 526.71 | 4.73 | 13709434.85 | 8 B | +| DummySerialize_And_Pack_Compressed | 100000 | 20,060.07 us | 228.341 us | 213.590 us | 1,448.88 | 17.92 | 4985028.25 | 50 B | +``` + +Key: +- DummySerializeOnly_BASELINE: `Overhead of non-packing related code.` +- DummySerialize_And_Pack: `Time taken to pack all messages without compression.` +- DummySerialize_And_Pack_Compressed: `Time taken to pack all messages with compression header.` + +## Real World Scenario Benchmarks + +The following benchmark measures the time taken to serialize: +- [A real world configuration file](https://github.com/Reloaded-Project/Reloaded-II/blob/32d5e132391d96814ea983cda231c271c43828e0/source/Reloaded.Mod.Loader.IO/Config/ModConfig.cs#L4) into JSON. +- And run it through the library. + +| Method | NumItems | Mean | Error | StdDev | Ratio | RatioSD | Operations/s | Gen 0 | Allocated | +|--------------------------------------------------------- |--------- |---------:|--------:|--------:|------:|--------:|-------------:|-----------:|--------------:| +| SerializeOnly_NoPack_To_SingleBuffer | 100000 | 155.6 ms | 2.76 ms | 2.59 ms | 1.00 | 0.00 | 642735.51 | - | 1,614,672 B | +| SerializeOnly_NoPack_To_BufferPerMessage | 100000 | 205.6 ms | 2.36 ms | 2.21 ms | 1.32 | 0.02 | 486273.70 | 6000.0000 | 26,400,624 B | +| Serialize_And_Pack | 100000 | 157.6 ms | 1.43 ms | 1.34 ms | 1.01 | 0.02 | 634681.18 | - | 252 B | +| Serialize_And_Pack_And_Handle | 100000 | 156.9 ms | 1.97 ms | 1.84 ms | 1.01 | 0.02 | 637216.58 | - | 420 B | +| Serialize_And_Pack_And_Handle_And_Unpack_And_Deserialize | 100000 | 581.8 ms | 7.88 ms | 7.37 ms | 3.74 | 0.06 | 171887.71 | 50000.0000 | 209,719,272 B | + +- SerializeOnly_NoPack_To_SingleBuffer: `Time taken to serialize all messages into a single memory buffer`. +- SerializeOnly_NoPack_To_BufferPerMessage: `Time taken to serialize all messages into 1 memory buffer per message`. +- Serialize_And_Pack: `Time taken to serialize and pack every message using the library.` +- Serialize_And_Pack_And_Handle: `Time taken to serialize, pack and send message via MessageDispatcher.` +- Serialize_And_Pack_And_Handle_And_Unpack_And_Deserialize: `Provided for completeness to measure theoretical throughput.` + +From these results we can extrapolate that: +- Overhead for packing a message is 1-2ms per 100,000 items; (`SerializeOnly_NoPack_To_SingleBuffer` - `Serialize_And_Pack`). + +## Additional Benchmarks + +Further benchmarks can be found in the `Reloaded.Messaging.Benchmarks` project. \ No newline at end of file diff --git a/docs/defining-structures.md b/docs/defining-structures.md new file mode 100644 index 0000000..61292c1 --- /dev/null +++ b/docs/defining-structures.md @@ -0,0 +1,72 @@ +# Creating Structures & Messages + +## Creating Message Structures + +!!! note + + The library heavily (ab)uses generics for optimal code generation and maximum throughput. + +First, a struct or class must be annotated by implementing the `IMessage` interface. + +```csharp +// Structures can specify their own custom serializer(s) and compressor(s). +// IMessage +public struct Vector3 : IMessage, DummyCompressor> +{ + // IMessage + public sbyte GetMessageType() => (sbyte)MessageType.Vector3; + public SystemTextJsonSerializer GetSerializer() => new(); + public DummyCompressor? GetCompressor() => null; + + // Example Data + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } +} +``` + +Each message must define the following methods: +- `GetMessageType`: Returns the unique message type. Valid values are between 0 and 127. +- `GetSerializer`: Returns an instance of the serializer used to serialize/deserialize the message. +- `GetCompressor`: Returns an instance of the compressor used to compress/decompress the message. + +The type of serializer (`TSerializer`) and type of compressor (`TCompressor`) are defined in the `IMessage` interface; here they are `SystemTextJsonSerializer` and `DummyCompressor` specifically. + +Return `null` for compressor if no compression is requested. + +## Pack Messages + +To pack an instance (including serialization & compression), call the extension method `Serialize()` in `Reloaded.Messaging.Messages.MessageWriterExtensions`. + +```csharp +var sample = new Vector3(0.0f, 1.0f, 2.0f); +using var serialized = sample.Serialize(ref sample); + +// Access message via `serialized.Span`. +// You can now e.g. send this message over the network. +Client.FirstPeer.Send(serialized.Span, DeliveryMethod.ReliableOrdered); +``` + +!!! danger + + Serialization result must be disposed before serializing another instance due to internal pooling. + The return value `ReusableSingletonMemoryStream` can have at most 1 instance per thread. + +Alternative lower level API: `MessageWriter`. + +## Unpack Messages + +!!! info + + Provided for completeness, this is usually automated for you. + Only low level API provided. + +```csharp +// Read message header. +HeaderReader.ReadHeader(message.Span, out var messageType, out var compressedSize, out var headerSize); + +// Create deserializer & deserialize +// Generic arguments to MessageReader are same as ones to IMessage. +var deserialize = new MessageReader, NullCompressor>(in structure); +Vector3 deserialized = deserialize.Deserialize(message.Span.Slice(headerSize), compressedSize); +``` \ No newline at end of file diff --git a/docs/images/reloaded-icon.png b/docs/images/reloaded-icon.png new file mode 100644 index 0000000..13e84a3 Binary files /dev/null and b/docs/images/reloaded-icon.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..475c1e0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,32 @@ +# Home + +
+

Reloaded: Messaging Module

+ +

+ Assert.Equal(funnyMessage, Dio) +

+
+ +# Introduction + +Reloaded.Networking is library that adds support for simple, high performance message packing to existing networking libraries. + +Specifically, it provides a minimal framework for performing the following tasks: + +- Asynchronous message processing for external networking libraries. +- Setting up host & client (for supported libraries). +- Sending/Receiving messages with (de)serialization and [optional] (de)compression. +- Automatically dispatching messages to appropriate handlers (per message type). + +It was originally created for Reloaded II, however has been extended in the hope of becoming a more general purpose library. + +This library is heavily optimized for achieving high throughput for messages `< 128KB`. + +## Characteristics +- High performance. (Memory pooling, low heap allocation). +- Low networking overhead. +- Custom serializer/compressor per class type. +- Simple message packing/protocol. +- 1 byte overhead for uncompressed, 5 bytes for compressed. +- Unsafe. \ No newline at end of file diff --git a/docs/message-structure.md b/docs/message-structure.md new file mode 100644 index 0000000..8f02914 --- /dev/null +++ b/docs/message-structure.md @@ -0,0 +1,50 @@ +# Message Structure + +Each message uses the following structure: + +``` +- [7 bits] Message Type +- [1 bit ] Compression Flag +- [0/4 bytes] Decompressed Size +- [Remaining bytes] Message +``` + +Messages are deliberately simple, to avoid unnecessary complexity. + +## Message Type +This is denoted by `TMessageType` within the library, and is user defined. +Each message type can be assigned 1 handler. + +Type: Signed byte +Values: `0x0 - 0x7F` +Mask: `0b0111_1111` + +## Compression Flag + +Set in leftmost bit of message type. +1 if the message contains compression, else 0. + +Type: Single bit +Values: `0x0 - 0x1` +Mask: `0b1000_0000` + +## Decompressed Size +Expected size after decompression. +Used for compressors to allow for memory pre-allocation. + +Type: Signed 32-bit integer. +Optional: Used only if struct defines a compressor. + +Values: +- `0x0 - 0x7FFFFFFF`: Expected size of decompressed message. + +Uses `Little Endian`. + +The resulting data is always compressed, regardless of whether it ended up being larger or smaller than the original in order to improve throughput (avoids a memory copy). + +Reason for signed vs unsigned lies in limitations of some languages (incl. .NET), memory pools often don't support 4GB arrays. + +## Message + +Raw message in bytes. +How message is stored depends on serializer. \ No newline at end of file diff --git a/docs/running-host.md b/docs/running-host.md new file mode 100644 index 0000000..0f81801 --- /dev/null +++ b/docs/running-host.md @@ -0,0 +1,69 @@ +# Running a Host + +The following article shows how to run an example host. +For a more complete example, look at Unit Tests. + + +## Create a Host + +!!! note + + The hosts in `Reloaded.Messaging` do the minimal amount of setup to enable asynchronous message handling. + They do not abstract the base libraries; for guidance on using them, please refer to their individual documentation(s). + +```csharp +Server = new LiteNetLibHost>(true, new MessageDispatcher()); +Client = new LiteNetLibHost>(true, new MessageDispatcher()); + +// Start listening and connect. +// This is LiteNetLib specific code. +Server.Manager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); +Client.Manager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); +Client.Manager.Connect(new IPEndPoint(IPAddress.Loopback, Server.Manager.LocalPort), DefaultPassword); +``` + +All pre-implemented hosts derive from the `IHost` class. +`TExtraData` is a generic type used to encapsulate the current state of the host. +In the case of `LiteNetLib`, it is called `LiteNetLibState`. + +## Sending Messages + +Each host implements the functions `SendFirstPeer` and `SendToAll`. + +Recall the serialization example from earlier: + +```csharp +var sample = new Vector3(0.0f, 1.0f, 2.0f); +using var serialized = sample.Serialize(ref sample); + +// Client is an IHost. +Client.SendFirstPeer(serialized.Span); +``` + +## Receiving Messages + +In order to receive messages, you must first register a handler for the message type. +You can do that by calling `AddToDispatcher` on the message type and providing: +- Class/struct that implements `IMsgRefAction`. +- The `Dispatcher` from the `IHost` instance. + +```csharp +var messageHandler = new LiteNetLibMessageHandler(); +messageHandler.Received.AddToDispatcher(messageHandler, ref Client.Dispatcher); + +// Class that will process received `Vector3`s from Host. +public class LiteNetLibMessageHandler : IMsgRefAction +{ + public void OnMessageReceive(ref Vector3 received, ref LiteNetLibState data) + { + // Executed on every Vector3 received from host. + // e.g. You can process message and send response with + // data.Peer.Send(); + } +} +``` + +Any instance of a class/struct that implements `IMsgRefAction` is fine. +You can of course implement multiple `IMsgRefAction` in the same class/struct too. + +The hosts are automatically configured to asynchronously receive messages, there is no need to manually call `Receive` or any similar function(s). \ No newline at end of file diff --git a/docs/serializers-compressors.md b/docs/serializers-compressors.md new file mode 100644 index 0000000..127bcd5 --- /dev/null +++ b/docs/serializers-compressors.md @@ -0,0 +1,111 @@ +# Serializers & Compressors + +## Existing Serializers + +All NuGet packages are prefixed with `Reloaded.Messaging` unless otherwise specified. + +| Serializer | NuGet Package | Format | Example Use Case | +|---------------------------------------------------------------------|---------------------------|---------|---------------------------------------------------------------------------------------------------| +| SystemTextJsonSerializer
SourceGeneratedSystemTextJsonSerializer | Extras.Runtime | JSON | Human Readable Data | +| MessagePackSerializer | Serializer.MessagePack | MsgPack | High Performance, Small Message Size | +| UnmanagedReloadedMemorySerializer | Serializer.ReloadedMemory | Binary | Raw struct/byte conversion.
When versioning is not needed and client/host use same endian. | + + +## Existing Compressors + +| Compressor | NuGet Package | Example Use Case | +|---------------------|----------------------|------------------------------------------------------------| +| BrotliCompressor | Extras.Runtime | Compressing structured data (e.g. JSON) | +| ZStandardCompressor | Compressor.ZStandard | Binary compression. Very good with pre-trained dictionary. | + +## Implementing your Own + +Implementing serializers and compressors is simple, create structs that implement the `ISerializer` and `ICompressor` interfaces. +Below are some example(s). + +### Example Serializer + +```csharp +/// +public struct SystemTextJsonSerializer : ISerializer +{ + /// + /// Serialization options. + /// + public JsonSerializerOptions Options { get; private set; } + + /// + /// Creates the System.Text.Json based serializer. + /// + public SystemTextJsonSerializer() { Options = new JsonSerializerOptions(); } + + /// + /// Creates the System.Text.Json based serializer. + /// + /// Options to use for serialization/deserialization. + public SystemTextJsonSerializer(JsonSerializerOptions serializerOptions) { Options = serializerOptions; } + + /// + public TStruct Deserialize(Span serialized) + { + return JsonSerializer.Deserialize(serialized, Options)!; + } + + /// + public void Serialize(ref TStruct item, IBufferWriter writer) + { + var write = Pool.JsonWriterPerThread(); + write.Reset(writer); + JsonSerializer.Serialize(write, item, Options); + } +} +``` + +### Example Compressor + +```csharp +/// +/// Provides brotli compression support. +/// +public struct BrotliCompressor : ICompressor +{ + private byte _quality; + private byte _window; + + /// + /// Creates the default brotli compressor. + /// + public BrotliCompressor() + { + _window = 22; + _quality = 9; + } + + /// + /// Quality of encoder. Between 0 and 11. Recommend 9 for size/speed ratio. + /// Size of window. + public BrotliCompressor(byte quality, byte window = 22) + { + _quality = quality; + _window = window; + } + + /// + public int GetMaxCompressedSize(int inputSize) => BrotliEncoder.GetMaxCompressedLength(inputSize); + + /// + public int Compress(Span uncompressedData, Span compressedData) + { + using var encoder = new BrotliEncoder(_quality, _window); + encoder.Compress(uncompressedData, compressedData, out _, out var bytesWritten, true); + return bytesWritten; + } + + /// + public void Decompress(Span compressedBuf, Span uncompressedBuf) + { + using var decoder = new BrotliDecoder(); + decoder.Decompress(compressedBuf, uncompressedBuf, out _, out _); + } +} +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..fa61ac5 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,50 @@ +site_name: Reloaded.Messaging Documentation +site_url: https://github.com/Reloaded-Project/Reloaded.Messaging + +repo_name: Reloaded-Project/Reloaded.Messaging +repo_url: https://github.com/Reloaded-Project/Reloaded.Messaging + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/Sewer56 + - icon: fontawesome/brands/twitter + link: https://twitter.com/TheSewer56 + +markdown_extensions: + - admonition + - tables + - pymdownx.details + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.tasklist + - def_list + - meta + - md_in_html + - attr_list + - footnotes + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg + +plugins: + - search + +theme: + name: material + palette: + primary: red + accent: red + scheme: slate + features: + - navigation.instant + +nav: + - Home: index.md + - Basic Usage: + - Defining Structures: defining-structures.md + - Running A Host: running-host.md + - Serializers & Compressors: serializers-compressors.md + - Benchmarks: benchmarks.md + - Internals: + - Message Structure: message-structure.md \ No newline at end of file