From a2e44a084a63df08f1ddd22d96d377053dd561b1 Mon Sep 17 00:00:00 2001 From: redouane Date: Sun, 18 Aug 2024 21:56:45 +0100 Subject: [PATCH 01/26] feat(logging): use `NullLoggerFactory` on Release config --- samples/PingPong/Program.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/samples/PingPong/Program.cs b/samples/PingPong/Program.cs index ccbe6dc..c9a171f 100644 --- a/samples/PingPong/Program.cs +++ b/samples/PingPong/Program.cs @@ -6,17 +6,23 @@ using EngineIO.Client; using Microsoft.Extensions.Logging; +#if DEBUG using Microsoft.Extensions.Logging.Console; +#else +using Microsoft.Extensions.Logging.Abstractions; +#endif namespace PingPong; internal class Program : IDisposable { + private readonly ILoggerFactory loggerFactory; private readonly ILogger logger; private static readonly CancellationTokenSource cts = new(); Program() { +#if DEBUG var loggerFactory = LoggerFactory.Create(builder => { builder.AddSimpleConsole(o => @@ -26,8 +32,12 @@ internal class Program : IDisposable o.ColorBehavior = LoggerColorBehavior.Enabled; }).SetMinimumLevel(LogLevel.Debug); }); - + logger = loggerFactory.CreateLogger(); +#else + this.loggerFactory = NullLoggerFactory.Instance; + this.logger = loggerFactory.CreateLogger(); +#endif Engine = new Engine((options) => { options.BaseAddress = "http://127.0.0.1:9854"; From c5890358699844d981644cf516ed6f1dca4046f5 Mon Sep 17 00:00:00 2001 From: redouane Date: Sat, 24 Aug 2024 16:12:32 +0100 Subject: [PATCH 02/26] feat(transport): use `MemoryPool` to buffer received messages --- src/EngineIO.Client/Transports/WebSocketTransport.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/EngineIO.Client/Transports/WebSocketTransport.cs b/src/EngineIO.Client/Transports/WebSocketTransport.cs index b15a87f..b9cf043 100644 --- a/src/EngineIO.Client/Transports/WebSocketTransport.cs +++ b/src/EngineIO.Client/Transports/WebSocketTransport.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.ObjectModel; using System.IO; using System.Net.WebSockets; @@ -104,13 +105,14 @@ public Task Disconnect() public async Task>> GetAsync(CancellationToken cancellationToken = default) { var packets = new Collection>(); - var stream = new MemoryStream(); - var buffer = new byte[16]; - + using var stream = new MemoryStream(); + using var rent = MemoryPool.Shared.Rent(16); + Memory buffer = rent.Memory; + try { await _receiveSemaphore.WaitAsync(CancellationToken.None); - WebSocketReceiveResult result; + ValueWebSocketReceiveResult result; do { result = await _client.ReceiveAsync(buffer, cancellationToken); @@ -121,7 +123,7 @@ public async Task>> GetAsync(Cancellatio break; } - await stream.WriteAsync(buffer, 0, result.Count, cancellationToken); + await stream.WriteAsync(buffer, cancellationToken); } while (!result.EndOfMessage); } finally From 5e3848188b1b64b8836eef22f90f6ee60a771b85 Mon Sep 17 00:00:00 2001 From: redouane Date: Sat, 24 Aug 2024 19:49:15 +0100 Subject: [PATCH 03/26] feat: prototype packet types and structure --- SocketIO.sln | 7 +++ src/SocketIO.Client/IO.cs | 19 +++++++ src/SocketIO.Client/Packets/PacketType.cs | 58 ++++++++++++++++++++++ src/SocketIO.Client/SocketIO.Client.csproj | 12 +++++ 4 files changed, 96 insertions(+) create mode 100644 src/SocketIO.Client/IO.cs create mode 100644 src/SocketIO.Client/Packets/PacketType.cs create mode 100644 src/SocketIO.Client/SocketIO.Client.csproj diff --git a/SocketIO.sln b/SocketIO.sln index aec678e..d3b1c57 100644 --- a/SocketIO.sln +++ b/SocketIO.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D8B5DE1E EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineIO.Client.Tests", "tests\EngineIO.Client.Tests\EngineIO.Client.Tests.csproj", "{5D824F05-5793-4768-99DF-E4D993F16B75}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SocketIO.Client", "src\SocketIO.Client\SocketIO.Client.csproj", "{222889F5-6A36-4A8A-8C78-A910E40D4F82}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,10 +38,15 @@ Global {5D824F05-5793-4768-99DF-E4D993F16B75}.Debug|Any CPU.Build.0 = Debug|Any CPU {5D824F05-5793-4768-99DF-E4D993F16B75}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D824F05-5793-4768-99DF-E4D993F16B75}.Release|Any CPU.Build.0 = Release|Any CPU + {222889F5-6A36-4A8A-8C78-A910E40D4F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {222889F5-6A36-4A8A-8C78-A910E40D4F82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {222889F5-6A36-4A8A-8C78-A910E40D4F82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {222889F5-6A36-4A8A-8C78-A910E40D4F82}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {D1C745FE-52AF-4145-8243-9FCA91BE7916} = {80C612B0-98E1-4E6B-AF10-5BB3087F0864} {4F6177C5-3234-4897-B61C-49C44D3E2EF8} = {577735D4-BC7E-464A-BC9B-93AD8B5B6738} {5D824F05-5793-4768-99DF-E4D993F16B75} = {D8B5DE1E-C83F-40A9-9D27-D2FBB3866F0A} + {222889F5-6A36-4A8A-8C78-A910E40D4F82} = {577735D4-BC7E-464A-BC9B-93AD8B5B6738} EndGlobalSection EndGlobal diff --git a/src/SocketIO.Client/IO.cs b/src/SocketIO.Client/IO.cs new file mode 100644 index 0000000..8db0a50 --- /dev/null +++ b/src/SocketIO.Client/IO.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +using EngineIO.Client; + +namespace SocketIO.Client; + +public class IO +{ + private readonly Engine _client; + + // Map namespace with its corresponding sid + private readonly Dictionary _namespaces = new(); + + public IO() + { + + } +} \ No newline at end of file diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs new file mode 100644 index 0000000..5d2c407 --- /dev/null +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -0,0 +1,58 @@ +using System; + +namespace SocketIO.Client.Packets; + +public enum PacketType : byte +{ + Connect = (byte)'0', + Disconnect = (byte)'1', + Event = (byte)'2', + Ack = (byte)'3', + ConnectError = (byte)'4', + BinaryEvent = (byte)'5', + BinaryAck = (byte)'6' +} + +/** + * SocketIO packet format: + * + * [<# of binary attachments>-][,][][JSON-stringified payload without binary] + * + binary attachments extracted + * + */ + +public struct Packet +{ + public static Packet ConnectPacket = new(PacketType.Connect, Array.Empty()); + + public static Packet DisconnectPacket = new(PacketType.Disconnect, Array.Empty()); + + public PacketType Type { get; private set; } + public string? Namespace { get; private set; } + public ReadOnlyMemory Data { get; private set; } + public uint Id { get; private set; } + + public Packet(PacketType type, ReadOnlyMemory data) + { + Type = type; + Data = data; + } + + public Packet(PacketType type, string @namespace, ReadOnlyMemory data) + : this(type, data) + { + Namespace = @namespace; + } + + public Packet(PacketType type, string @namespace, ReadOnlyMemory data, uint id) + : this(type, @namespace, data) + { + Id = id; + } + + public Packet(PacketType type, ReadOnlyMemory data, uint id) + : this(type, data) + { + Id = id; + } +} \ No newline at end of file diff --git a/src/SocketIO.Client/SocketIO.Client.csproj b/src/SocketIO.Client/SocketIO.Client.csproj new file mode 100644 index 0000000..d8fe417 --- /dev/null +++ b/src/SocketIO.Client/SocketIO.Client.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.1 + enable + + + + + + + From 63f0ccebf5ac6b1a5e6f5050568db5a13b8f3fe4 Mon Sep 17 00:00:00 2001 From: redouane Date: Sun, 25 Aug 2024 16:05:26 +0100 Subject: [PATCH 04/26] feat: prototype client API surface --- src/SocketIO.Client/IO.cs | 42 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/SocketIO.Client/IO.cs b/src/SocketIO.Client/IO.cs index 8db0a50..9bd7888 100644 --- a/src/SocketIO.Client/IO.cs +++ b/src/SocketIO.Client/IO.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using EngineIO.Client; @@ -8,12 +11,47 @@ namespace SocketIO.Client; public class IO { private readonly Engine _client; + private readonly string _baseUrl; // Map namespace with its corresponding sid private readonly Dictionary _namespaces = new(); - public IO() + public IO(string baseUrl) { - + this._baseUrl = baseUrl; + + this._client = new Engine((config) => + { + config.BaseAddress = baseUrl; + config.AutoUpgrade = true; + + // TODO: allow passing custom headers and queries + }); + } + + public Task Connect(string? @namespace = default) + { + return Task.CompletedTask; + } + + public async IAsyncEnumerable> ListenAsync( + string? @namespace = default, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + yield return Array.Empty(); + } + + public Task SendAsync(string? @namespace, ReadOnlyMemory data) + { + return Task.CompletedTask; + } + + public Task SendAsync(string? @namespace, T data) + { + return Task.CompletedTask; + } + + public Task SendAsync(string? @namespace, T[] data) + { + return Task.CompletedTask; } } \ No newline at end of file From b4ddad8710bb939407f0761b1f12ab974febad20 Mon Sep 17 00:00:00 2001 From: redouane Date: Mon, 2 Sep 2024 00:23:59 +0100 Subject: [PATCH 05/26] feat: make namespace as the last optional argument in `SendAsync` methods --- src/SocketIO.Client/IO.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SocketIO.Client/IO.cs b/src/SocketIO.Client/IO.cs index 9bd7888..005ba71 100644 --- a/src/SocketIO.Client/IO.cs +++ b/src/SocketIO.Client/IO.cs @@ -40,17 +40,17 @@ public async IAsyncEnumerable> ListenAsync( yield return Array.Empty(); } - public Task SendAsync(string? @namespace, ReadOnlyMemory data) + public Task SendAsync(ReadOnlyMemory data, string? @namespace = null) { return Task.CompletedTask; } - public Task SendAsync(string? @namespace, T data) + public Task SendAsync(T data, string? @namespace = null) { return Task.CompletedTask; } - public Task SendAsync(string? @namespace, T[] data) + public Task SendAsync(T[] data, string? @namespace = null) { return Task.CompletedTask; } From 590a1b31a80e89f477137c35a43b489a57217ea3 Mon Sep 17 00:00:00 2001 From: redouane Date: Fri, 27 Sep 2024 22:34:20 +0100 Subject: [PATCH 06/26] chore: add TODO --- src/EngineIO.Client/Transports/PacketExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/EngineIO.Client/Transports/PacketExtensions.cs b/src/EngineIO.Client/Transports/PacketExtensions.cs index 940dcdf..10603a0 100644 --- a/src/EngineIO.Client/Transports/PacketExtensions.cs +++ b/src/EngineIO.Client/Transports/PacketExtensions.cs @@ -20,6 +20,8 @@ public static ReadOnlyMemory ToPlaintextPacket(this Packet packet) throw new InvalidOperationException("Wrong packet format"); } + // TODO: revisit this implementation for potential improvements + var payload = new byte[1 + packet.Body.Length]; payload[0] = (byte)packet.Type; for (var i = 1; i <= packet.Body.Length; i++) @@ -44,6 +46,8 @@ public static ReadOnlyMemory ToBinaryPacket(this Packet packet, IEncoder e throw new InvalidOperationException("Wrong packet format"); } + // TODO: revisit this implementation for potential improvement + var encodedBody = encoder.Encode(packet.Body, Encoding.UTF8); var payload = new byte[1 + encodedBody.Length]; payload[0] = (byte)'b'; From f6bf4cb71e0a1586c2fbfa66047265a727876515 Mon Sep 17 00:00:00 2001 From: redouane Date: Fri, 27 Sep 2024 22:35:47 +0100 Subject: [PATCH 07/26] feat: add socket.io packet serializer --- src/SocketIO.Client/IO.cs | 19 +-- src/SocketIO.Client/Packets/PacketData.cs | 68 +++++++++++ src/SocketIO.Client/Packets/PacketType.cs | 139 ++++++++++++++++++---- 3 files changed, 193 insertions(+), 33 deletions(-) create mode 100644 src/SocketIO.Client/Packets/PacketData.cs diff --git a/src/SocketIO.Client/IO.cs b/src/SocketIO.Client/IO.cs index 005ba71..5deb907 100644 --- a/src/SocketIO.Client/IO.cs +++ b/src/SocketIO.Client/IO.cs @@ -6,6 +6,8 @@ using EngineIO.Client; +using SocketIO.Client.Packets; + namespace SocketIO.Client; public class IO @@ -37,21 +39,24 @@ public Task Connect(string? @namespace = default) public async IAsyncEnumerable> ListenAsync( string? @namespace = default, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - yield return Array.Empty(); + yield return await Task.FromResult(Array.Empty()); } public Task SendAsync(ReadOnlyMemory data, string? @namespace = null) { + var packet = new Packet(PacketType.Event) { Namespace = @namespace, }; + packet.AddItem(data); + + // TODO: return Task.CompletedTask; } - public Task SendAsync(T data, string? @namespace = null) - { - return Task.CompletedTask; - } - - public Task SendAsync(T[] data, string? @namespace = null) + public Task SendAsync(T data, string? @namespace = null) where T : class { + // TODO: send as string data + var packet = new Packet(PacketType.Event) { Namespace = @namespace, }; + packet.AddItem(data); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/SocketIO.Client/Packets/PacketData.cs b/src/SocketIO.Client/Packets/PacketData.cs new file mode 100644 index 0000000..12617d4 --- /dev/null +++ b/src/SocketIO.Client/Packets/PacketData.cs @@ -0,0 +1,68 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SocketIO.Client.Packets; + +internal interface PacketData +{ + void Serialize(Utf8JsonWriter stream); + +} +internal sealed class TextPacketData : PacketData +{ + public string Data { get; } + + public TextPacketData(string data) + { + Data = data; + } + + public void Serialize(Utf8JsonWriter stream) + { + stream.WriteStringValue(this.Data); + } +} + +internal sealed class JsonPacketData : PacketData where T : class +{ + public T Data { get; } + + public JsonPacketData(T data) + { + Data = data; + } + + public void Serialize(Utf8JsonWriter stream) + { + JsonSerializer.Serialize(stream, Data); + } +} + +internal sealed class BinaryPacketData : PacketData +{ + public BinaryPacketData(int id, ReadOnlyMemory data) + { + Id = id; + Data = data; + } + + [JsonIgnore] + public ReadOnlyMemory Data { get; } + + [JsonPropertyName("_placeholder")] + public bool Placeholder => true; + + [JsonPropertyName("num")] + public int Id { get; set; } + + public ReadOnlySpan Serialize() + { + throw new NotImplementedException(); + } + + public void Serialize(Utf8JsonWriter stream) + { + JsonSerializer.Serialize(stream, this); + } +} diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs index 5d2c407..32ab87b 100644 --- a/src/SocketIO.Client/Packets/PacketType.cs +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -1,58 +1,145 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; namespace SocketIO.Client.Packets; public enum PacketType : byte { - Connect = (byte)'0', - Disconnect = (byte)'1', - Event = (byte)'2', - Ack = (byte)'3', - ConnectError = (byte)'4', - BinaryEvent = (byte)'5', - BinaryAck = (byte)'6' + Connect = 0x30, + Disconnect = 0x31, + Event = 0x32, + Ack = 0x33, + ConnectError = 0x34, + BinaryEvent = 0x35, + BinaryAck = 0x36 } /** * SocketIO packet format: - * + * + * Initial Packet: * [<# of binary attachments>-][,][][JSON-stringified payload without binary] + * + * Next Packets containing binary data * + binary attachments extracted * */ -public struct Packet +public class Packet { - public static Packet ConnectPacket = new(PacketType.Connect, Array.Empty()); - - public static Packet DisconnectPacket = new(PacketType.Disconnect, Array.Empty()); + public static Packet ConnectPacket = new(PacketType.Connect); - public PacketType Type { get; private set; } - public string? Namespace { get; private set; } - public ReadOnlyMemory Data { get; private set; } - public uint Id { get; private set; } + public static Packet DisconnectPacket = new(PacketType.Disconnect); - public Packet(PacketType type, ReadOnlyMemory data) + public PacketType Type { get; } + public string? Namespace { get; internal set; } + public uint? AckId { get; } + + private List _data = new(); + + public Packet(PacketType type) { Type = type; - Data = data; + Namespace = "/"; } - public Packet(PacketType type, string @namespace, ReadOnlyMemory data) - : this(type, data) + public Packet(PacketType type, string @namespace) + : this(type) { Namespace = @namespace; } - public Packet(PacketType type, string @namespace, ReadOnlyMemory data, uint id) - : this(type, @namespace, data) + public Packet(PacketType type, string @namespace, uint ackId) + : this(type, @namespace) + { + AckId = ackId; + } + + /// + /// Add plain text data to packet + /// + /// Plain text data + public void AddItem(string data) + { + this._data.Add(new TextPacketData(data)); + } + + /// + /// Add binary data + /// + /// Binary data + public void AddItem(ReadOnlyMemory data) { - Id = id; + int id = this._data.OfType().Count(); + this._data.Add(new BinaryPacketData(id, data)); } - public Packet(PacketType type, ReadOnlyMemory data, uint id) - : this(type, data) + /// + /// Add Json serializable POCO + /// + /// Data instance + /// Data type + public void AddItem(T data) where T : class + { + this._data.Add(new JsonPacketData(data)); + } + + internal ReadOnlySpan Encode() { - Id = id; + using (var stream = new MemoryStream()) + { + // Write packet header + var headerWriter = new StreamWriter(stream); + + headerWriter.Write((char)this.Type); + if (Type is PacketType.BinaryEvent or PacketType.BinaryAck && this._data.Count > 0) + { + var count = this._data.OfType().Count(); + headerWriter.Write(count); + headerWriter.Write('-'); + } + + // Write packet namespace + headerWriter.Write(this.Namespace); + headerWriter.Write(','); + + // Write acknowledgement id if set + if (this.AckId.HasValue) + { + headerWriter.Write(this.AckId); + } + headerWriter.Flush(); + + // Write packet content + var dataWriter = new Utf8JsonWriter(stream); + dataWriter.WriteStartArray(); + foreach (PacketData item in this._data) + { + if (item is TextPacketData plainText) + { + plainText.Serialize(dataWriter); + } + else if (item is BinaryPacketData binary) + { + binary.Serialize(dataWriter); + } + else + { + item.Serialize(dataWriter); + } + } + dataWriter.WriteEndArray(); + dataWriter.Flush(); + + stream.Seek(0, SeekOrigin.Begin); + var data = stream.ToArray(); + + headerWriter.Dispose(); + + return data; + } } } \ No newline at end of file From c9eda1412dafce44fe8f8e38ba06301fc759624a Mon Sep 17 00:00:00 2001 From: redouane Date: Fri, 27 Sep 2024 22:36:09 +0100 Subject: [PATCH 08/26] chore: expose assembly to tests --- src/SocketIO.Client/AssemblyInfo.cs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/SocketIO.Client/AssemblyInfo.cs diff --git a/src/SocketIO.Client/AssemblyInfo.cs b/src/SocketIO.Client/AssemblyInfo.cs new file mode 100644 index 0000000..7cf8eef --- /dev/null +++ b/src/SocketIO.Client/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SocketIO.Client.Tests")] \ No newline at end of file From c3e506bde4b0d7c50320bf68156429e0fd1d2163 Mon Sep 17 00:00:00 2001 From: redouane Date: Fri, 27 Sep 2024 22:39:49 +0100 Subject: [PATCH 09/26] chore: clean up --- src/SocketIO.Client/Packets/PacketData.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/SocketIO.Client/Packets/PacketData.cs b/src/SocketIO.Client/Packets/PacketData.cs index 12617d4..fd73b6a 100644 --- a/src/SocketIO.Client/Packets/PacketData.cs +++ b/src/SocketIO.Client/Packets/PacketData.cs @@ -7,8 +7,8 @@ namespace SocketIO.Client.Packets; internal interface PacketData { void Serialize(Utf8JsonWriter stream); - } + internal sealed class TextPacketData : PacketData { public string Data { get; } @@ -56,11 +56,6 @@ public BinaryPacketData(int id, ReadOnlyMemory data) [JsonPropertyName("num")] public int Id { get; set; } - public ReadOnlySpan Serialize() - { - throw new NotImplementedException(); - } - public void Serialize(Utf8JsonWriter stream) { JsonSerializer.Serialize(stream, this); From 56715c5d884be4e5e79dd320b95cecd62fe3f786 Mon Sep 17 00:00:00 2001 From: redouane Date: Sat, 28 Sep 2024 12:13:07 +0100 Subject: [PATCH 10/26] feat: fix usings, clean up and add TODOs --- src/SocketIO.Client/Packets/PacketType.cs | 83 +++++++++++------------ 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs index 32ab87b..6029737 100644 --- a/src/SocketIO.Client/Packets/PacketType.cs +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -64,6 +64,7 @@ public Packet(PacketType type, string @namespace, uint ackId) /// Plain text data public void AddItem(string data) { + // TODO: check if packet type is not binary event, if it is, we should throw this._data.Add(new TextPacketData(data)); } @@ -73,6 +74,8 @@ public void AddItem(string data) /// Binary data public void AddItem(ReadOnlyMemory data) { + // TODO: check if packet type is binary event. if not, we should throw + int id = this._data.OfType().Count(); this._data.Add(new BinaryPacketData(id, data)); } @@ -89,57 +92,51 @@ public void AddItem(T data) where T : class internal ReadOnlySpan Encode() { - using (var stream = new MemoryStream()) + using var stream = new MemoryStream(); + // write packet header + using var headerWriter = new StreamWriter(stream); + + // write packet type + headerWriter.Write((char)this.Type); + if (Type is PacketType.BinaryEvent or PacketType.BinaryAck && this._data.Count > 0) { - // Write packet header - var headerWriter = new StreamWriter(stream); + var count = this._data.OfType().Count(); + headerWriter.Write(count); + headerWriter.Write('-'); + } + + // write packet namespace + headerWriter.Write(this.Namespace); + headerWriter.Write(','); + + // write acknowledgement id if set + if (this.AckId.HasValue) + { + headerWriter.Write(this.AckId); + } + headerWriter.Flush(); - headerWriter.Write((char)this.Type); - if (Type is PacketType.BinaryEvent or PacketType.BinaryAck && this._data.Count > 0) + // write packet content + var dataWriter = new Utf8JsonWriter(stream); + dataWriter.WriteStartArray(); + foreach (PacketData item in this._data) + { + if (item is TextPacketData plainText) { - var count = this._data.OfType().Count(); - headerWriter.Write(count); - headerWriter.Write('-'); + plainText.Serialize(dataWriter); } - - // Write packet namespace - headerWriter.Write(this.Namespace); - headerWriter.Write(','); - - // Write acknowledgement id if set - if (this.AckId.HasValue) + else if (item is BinaryPacketData binary) { - headerWriter.Write(this.AckId); + binary.Serialize(dataWriter); } - headerWriter.Flush(); - - // Write packet content - var dataWriter = new Utf8JsonWriter(stream); - dataWriter.WriteStartArray(); - foreach (PacketData item in this._data) + else { - if (item is TextPacketData plainText) - { - plainText.Serialize(dataWriter); - } - else if (item is BinaryPacketData binary) - { - binary.Serialize(dataWriter); - } - else - { - item.Serialize(dataWriter); - } + item.Serialize(dataWriter); } - dataWriter.WriteEndArray(); - dataWriter.Flush(); - - stream.Seek(0, SeekOrigin.Begin); - var data = stream.ToArray(); - - headerWriter.Dispose(); - - return data; } + dataWriter.WriteEndArray(); + dataWriter.Flush(); + + return stream.ToArray(); } } \ No newline at end of file From 772ccf0163104d528bd4d83b54d48eeda67339c4 Mon Sep 17 00:00:00 2001 From: redouane Date: Sat, 28 Sep 2024 20:37:59 +0100 Subject: [PATCH 11/26] feat: refactor packet serialization and send implementation --- src/EngineIO.Client/Engine.cs | 8 +++++--- src/EngineIO.Client/Packets/Packet.cs | 14 +++++--------- .../Transports/HttpPollingTransport.cs | 2 +- src/EngineIO.Client/Transports/PacketExtensions.cs | 4 ++-- .../Transports/WebSocketTransport.cs | 4 ++-- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/EngineIO.Client/Engine.cs b/src/EngineIO.Client/Engine.cs index 3e1ad12..56e26b6 100644 --- a/src/EngineIO.Client/Engine.cs +++ b/src/EngineIO.Client/Engine.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Runtime.CompilerServices; +using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -109,7 +110,7 @@ private async Task PollAsync() { #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - _transport.SendAsync(Packet.PongPacket.ToPlaintextPacket(), PacketFormat.PlainText, + _transport.SendAsync(Packet.PongPacket.ToWirePacket(), PacketFormat.PlainText, _pollingCancellationTokenSource.Token); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed @@ -172,7 +173,8 @@ public async IAsyncEnumerable ListenAsync([EnumeratorCancellation] Cance /// public async Task SendAsync(string text, CancellationToken cancellationToken = default) { - var packet = Packet.CreateMessagePacket(text).ToPlaintextPacket(); + var body = Encoding.UTF8.GetBytes(text); + var packet = Packet.CreatePacket(PacketFormat.PlainText, body).ToWirePacket(); await _transport.SendAsync(packet, PacketFormat.PlainText, cancellationToken); } @@ -183,7 +185,7 @@ public async Task SendAsync(string text, CancellationToken cancellationToken = d /// public async Task SendAsync(ReadOnlyMemory binary, CancellationToken cancellationToken = default) { - var packet = Packet.CreateBinaryPacket(binary).ToBinaryPacket(_base64Encoder); + var packet = Packet.CreatePacket(PacketFormat.Binary, binary).ToWirePacket(_base64Encoder); await _transport.SendAsync(packet, PacketFormat.Binary, cancellationToken); } } \ No newline at end of file diff --git a/src/EngineIO.Client/Packets/Packet.cs b/src/EngineIO.Client/Packets/Packet.cs index 912f34a..9055869 100644 --- a/src/EngineIO.Client/Packets/Packet.cs +++ b/src/EngineIO.Client/Packets/Packet.cs @@ -1,5 +1,4 @@ using System; -using System.Text; namespace EngineIO.Client.Packets; @@ -42,15 +41,11 @@ public static bool TryParse(ReadOnlyMemory data, out Packet packet) return true; } - public static Packet CreateMessagePacket(string text) + public static Packet CreatePacket(PacketFormat format, ReadOnlyMemory body) { - var body = Encoding.UTF8.GetBytes(text); - return new Packet(PacketFormat.PlainText, PacketType.Message, body); - } - - public static Packet CreateBinaryPacket(ReadOnlyMemory body) - { - return new Packet(PacketFormat.Binary, PacketType.Message, body); + return format == PacketFormat.Binary ? + new Packet(PacketFormat.Binary, PacketType.Message, body) + : new Packet(PacketFormat.PlainText, PacketType.Message, body); } public Packet(PacketFormat format, PacketType type, ReadOnlyMemory body) @@ -58,6 +53,7 @@ public Packet(PacketFormat format, PacketType type, ReadOnlyMemory body) Format = format; Type = format == PacketFormat.Binary ? PacketType.Message : type; Body = body; + // count body content bytes length and packet type byte Length = body.Length + 1; } diff --git a/src/EngineIO.Client/Transports/HttpPollingTransport.cs b/src/EngineIO.Client/Transports/HttpPollingTransport.cs index 87b2c44..b498807 100644 --- a/src/EngineIO.Client/Transports/HttpPollingTransport.cs +++ b/src/EngineIO.Client/Transports/HttpPollingTransport.cs @@ -59,7 +59,7 @@ public async Task Disconnect() { if (_connected) { - await SendAsync(Packet.ClosePacket.ToPlaintextPacket(), PacketFormat.PlainText); + await SendAsync(Packet.ClosePacket.ToWirePacket(), PacketFormat.PlainText); } _connected = false; diff --git a/src/EngineIO.Client/Transports/PacketExtensions.cs b/src/EngineIO.Client/Transports/PacketExtensions.cs index 10603a0..9b968c1 100644 --- a/src/EngineIO.Client/Transports/PacketExtensions.cs +++ b/src/EngineIO.Client/Transports/PacketExtensions.cs @@ -13,7 +13,7 @@ public static class PacketExtensions /// Packet instance /// /// - public static ReadOnlyMemory ToPlaintextPacket(this Packet packet) + public static ReadOnlyMemory ToWirePacket(this Packet packet) { if (packet.Format != PacketFormat.PlainText && packet.Type != PacketType.Message) { @@ -39,7 +39,7 @@ public static ReadOnlyMemory ToPlaintextPacket(this Packet packet) /// Binary packet encoder /// /// - public static ReadOnlyMemory ToBinaryPacket(this Packet packet, IEncoder encoder) + public static ReadOnlyMemory ToWirePacket(this Packet packet, IEncoder encoder) { if (packet.Format != PacketFormat.Binary && packet.Type != PacketType.Message) { diff --git a/src/EngineIO.Client/Transports/WebSocketTransport.cs b/src/EngineIO.Client/Transports/WebSocketTransport.cs index b9cf043..32d89ff 100644 --- a/src/EngineIO.Client/Transports/WebSocketTransport.cs +++ b/src/EngineIO.Client/Transports/WebSocketTransport.cs @@ -71,7 +71,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) await _client.ConnectAsync(_uri, cancellationToken); // ping probe - await SendAsync(Packet.PingProbePacket.ToPlaintextPacket(), PacketFormat.PlainText, cancellationToken); + await SendAsync(Packet.PingProbePacket.ToWirePacket(), PacketFormat.PlainText, cancellationToken); // pong probe var data = await GetAsync(cancellationToken); @@ -86,7 +86,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) } // upgrade - await SendAsync(Packet.UpgradePacket.ToPlaintextPacket(), PacketFormat.PlainText, cancellationToken); + await SendAsync(Packet.UpgradePacket.ToWirePacket(), PacketFormat.PlainText, cancellationToken); _connected = true; } From 32722da8015876f46a00ae706b6ffda716a1c9bd Mon Sep 17 00:00:00 2001 From: redouane Date: Mon, 30 Sep 2024 13:15:12 +0100 Subject: [PATCH 12/26] fix: fix tests and samples --- samples/PingPong/Program.cs | 2 +- tests/EngineIO.Client.Tests/Packets/PacketTests.cs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/samples/PingPong/Program.cs b/samples/PingPong/Program.cs index c9a171f..b8688ed 100644 --- a/samples/PingPong/Program.cs +++ b/samples/PingPong/Program.cs @@ -58,7 +58,7 @@ private static async Task Main(string[] args) program.logger.LogDebug("Client Starting..."); await program.Engine.ConnectAsync(cts.Token); - await Task.WhenAll(Task.Run(program.EmitAsync), Task.Run(program.ListenAsync)); + await Task.WhenAll(Task.Run(program.EmitAsync, cts.Token), Task.Run(program.ListenAsync, cts.Token)); program.logger.LogDebug("Disconnecting..."); await program.Engine.DisconnectAsync(); diff --git a/tests/EngineIO.Client.Tests/Packets/PacketTests.cs b/tests/EngineIO.Client.Tests/Packets/PacketTests.cs index 09332d9..6a85c03 100644 --- a/tests/EngineIO.Client.Tests/Packets/PacketTests.cs +++ b/tests/EngineIO.Client.Tests/Packets/PacketTests.cs @@ -1,5 +1,3 @@ -using System.Text; - using EngineIO.Client.Packets; using EngineIO.Client.Transports; @@ -14,7 +12,7 @@ void Create_Payload_From_Plaintext_Message() PacketFormat.PlainText, PacketType.Message, new[] { (byte)'H', (byte)'i' }) - .ToPlaintextPacket(); + .ToWirePacket(); Assert.Equal((byte)PacketType.Message, packet.Span[0]); Assert.Equal((byte)'H', packet.Span[1]); @@ -24,13 +22,14 @@ void Create_Payload_From_Plaintext_Message() [Fact] void Create_Payload_From_Binary_Message() { - var body = Encoding.UTF8.GetBytes("Hi"); + var body = "Hi"u8.ToArray(); var base64 = Convert.ToBase64String(body); + var packet = new Packet( PacketFormat.Binary, PacketType.Message, new[] { (byte)'H', (byte)'i' }) - .ToBinaryPacket(new Base64Encoder()); + .ToWirePacket(new Base64Encoder()); Assert.Equal((byte)'b', packet.Span[0]); Assert.Equal((byte)base64[0], packet.Span[1]); @@ -65,6 +64,6 @@ void Parse_Should_Throw_Invalid_Packet_Type() void PingProbePacket_Should_Be_Valid() { var packet = Packet.PingProbePacket; - Assert.Equal((byte)PacketType.Ping, packet.ToPlaintextPacket().Span[0]); + Assert.Equal((byte)PacketType.Ping, packet.ToWirePacket().Span[0]); } } \ No newline at end of file From 79d6424a652ac2902f9ee7827f94ac8a63b39c89 Mon Sep 17 00:00:00 2001 From: redouane Date: Mon, 30 Sep 2024 15:48:42 +0100 Subject: [PATCH 13/26] feat(socket.io): refactoring and documentation - add packet `event` name property - refactor `IPacketData` interface - add docs comments - clean up --- src/SocketIO.Client/Packets/PacketData.cs | 2 +- src/SocketIO.Client/Packets/PacketType.cs | 74 +++++++++++++++-------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/SocketIO.Client/Packets/PacketData.cs b/src/SocketIO.Client/Packets/PacketData.cs index fd73b6a..3fed736 100644 --- a/src/SocketIO.Client/Packets/PacketData.cs +++ b/src/SocketIO.Client/Packets/PacketData.cs @@ -4,7 +4,7 @@ namespace SocketIO.Client.Packets; -internal interface PacketData +internal interface IPacketData { void Serialize(Utf8JsonWriter stream); } diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs index 6029737..d37ad9b 100644 --- a/src/SocketIO.Client/Packets/PacketType.cs +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -8,12 +8,39 @@ namespace SocketIO.Client.Packets; public enum PacketType : byte { + /// + /// Connect packet type + /// Connect = 0x30, + + /// + /// Disconnect packet type + /// Disconnect = 0x31, + + /// + /// Event packet type with Plaintext/JSON data + /// Event = 0x32, + + /// + /// Acknowledgement packet type with plaintext/json data + /// Ack = 0x33, + + /// + /// Connection error packet type + /// ConnectError = 0x34, + + /// + /// Event packet type with binary data + /// BinaryEvent = 0x35, + + /// + /// Acknowledgement packet type with binary data + /// BinaryAck = 0x36 } @@ -37,23 +64,30 @@ public class Packet public PacketType Type { get; } public string? Namespace { get; internal set; } public uint? AckId { get; } + + public string Event { get; } - private List _data = new(); + private readonly List _data = new(); + private readonly MemoryStream _buffer = new(); + public Packet(PacketType type) + : this(type, "/", "message") { Type = type; Namespace = "/"; } - public Packet(PacketType type, string @namespace) - : this(type) + public Packet(PacketType type, string @namespace, string @event) { + Type = type; Namespace = @namespace; + Event = @event; + this.AddItem(Event); } - public Packet(PacketType type, string @namespace, uint ackId) - : this(type, @namespace) + public Packet(PacketType type, string @namespace, string @event, uint ackId) + : this(type, @namespace, @event) { AckId = ackId; } @@ -90,12 +124,16 @@ public void AddItem(T data) where T : class this._data.Add(new JsonPacketData(data)); } - internal ReadOnlySpan Encode() + /// + /// Serialize the packet and return the underlying memory stream which contains the raw packet bytes. + /// + /// The underlying `MemoryStream` + internal MemoryStream Serialize() { - using var stream = new MemoryStream(); // write packet header - using var headerWriter = new StreamWriter(stream); - + using var headerWriter = new StreamWriter(this._buffer); + using var dataWriter = new Utf8JsonWriter(this._buffer); + // write packet type headerWriter.Write((char)this.Type); if (Type is PacketType.BinaryEvent or PacketType.BinaryAck && this._data.Count > 0) @@ -117,26 +155,14 @@ internal ReadOnlySpan Encode() headerWriter.Flush(); // write packet content - var dataWriter = new Utf8JsonWriter(stream); dataWriter.WriteStartArray(); - foreach (PacketData item in this._data) + foreach (IPacketData item in this._data) { - if (item is TextPacketData plainText) - { - plainText.Serialize(dataWriter); - } - else if (item is BinaryPacketData binary) - { - binary.Serialize(dataWriter); - } - else - { - item.Serialize(dataWriter); - } + item.Serialize(dataWriter); } dataWriter.WriteEndArray(); dataWriter.Flush(); - return stream.ToArray(); + return this._buffer; } } \ No newline at end of file From b7dbd0219dc54a68d739d70484b45a0718dfae0e Mon Sep 17 00:00:00 2001 From: redouane Date: Mon, 30 Sep 2024 15:51:18 +0100 Subject: [PATCH 14/26] feat: add socket.io server and update npm setup --- package-lock.json | 175 ++++++++++++---- package.json | 6 +- .../simple-engine-io-server/package-lock.json | 194 ------------------ samples/simple-socket-io-server/index.js | 18 ++ samples/simple-socket-io-server/package.json | 15 ++ 5 files changed, 172 insertions(+), 236 deletions(-) delete mode 100644 samples/simple-engine-io-server/package-lock.json create mode 100644 samples/simple-socket-io-server/index.js create mode 100644 samples/simple-socket-io-server/package.json diff --git a/package-lock.json b/package-lock.json index 5bed53d..87fe015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,40 +9,44 @@ "version": "1.0.0", "license": "ISC", "workspaces": [ - "samples/simple-engine-io-server" + "samples/simple-engine-io-server", + "samples/simple-socket-io-server" ] }, - "node_modules/simple-engine-io-server": { - "resolved": "samples/simple-engine-io-server", - "link": true - }, - "samples/simple-engine-io-server": { - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "engine.io": "^6.5.4" - } + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" }, - "samples/simple-engine-io-server/node_modules/@types/cookie": { + "node_modules/@types/cookie": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "license": "MIT" }, - "samples/simple-engine-io-server/node_modules/@types/cors": { + "node_modules/@types/cors": { "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "license": "MIT", "dependencies": { "@types/node": "*" } }, - "samples/simple-engine-io-server/node_modules/@types/node": { - "version": "20.11.20", + "node_modules/@types/node": { + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, - "samples/simple-engine-io-server/node_modules/accepts": { + "node_modules/accepts": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -52,22 +56,28 @@ "node": ">= 0.6" } }, - "samples/simple-engine-io-server/node_modules/base64id": { + "node_modules/base64id": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } }, - "samples/simple-engine-io-server/node_modules/cookie": { + "node_modules/cookie": { "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "samples/simple-engine-io-server/node_modules/cors": { + "node_modules/cors": { "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -77,11 +87,13 @@ "node": ">= 0.10" } }, - "samples/simple-engine-io-server/node_modules/debug": { - "version": "4.3.4", + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -92,8 +104,10 @@ } } }, - "samples/simple-engine-io-server/node_modules/engine.io": { - "version": "6.5.4", + "node_modules/engine.io": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.1.tgz", + "integrity": "sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==", "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", @@ -105,28 +119,34 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" }, "engines": { "node": ">=10.2.0" } }, - "samples/simple-engine-io-server/node_modules/engine.io-parser": { - "version": "5.2.2", + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "license": "MIT", "engines": { "node": ">=10.0.0" } }, - "samples/simple-engine-io-server/node_modules/mime-db": { + "node_modules/mime-db": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "samples/simple-engine-io-server/node_modules/mime-types": { + "node_modules/mime-types": { "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -135,44 +155,105 @@ "node": ">= 0.6" } }, - "samples/simple-engine-io-server/node_modules/ms": { - "version": "2.1.2", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "samples/simple-engine-io-server/node_modules/negotiator": { + "node_modules/negotiator": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "samples/simple-engine-io-server/node_modules/object-assign": { + "node_modules/object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "samples/simple-engine-io-server/node_modules/undici-types": { - "version": "5.26.5", + "node_modules/simple-engine-io-server": { + "resolved": "samples/simple-engine-io-server", + "link": true + }, + "node_modules/simple-socket-io-server": { + "resolved": "samples/simple-socket-io-server", + "link": true + }, + "node_modules/socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, - "samples/simple-engine-io-server/node_modules/vary": { + "node_modules/vary": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "samples/simple-engine-io-server/node_modules/ws": { - "version": "8.11.0", + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -182,6 +263,20 @@ "optional": true } } + }, + "samples/simple-engine-io-server": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "engine.io": "^6.5.4" + } + }, + "samples/simple-socket-io-server": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "socket.io": "^4.8.0" + } } } } diff --git a/package.json b/package.json index 4ec4c88..2319000 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,13 @@ "description": "Socket.IO .NET client samples test server and tools", "directories": {}, "scripts": { - "start:server": "npm start -w samples/simple-engine-io-server" + "start:engine": "npm start -w samples/simple-engine-io-server", + "start:socket": "npm start -w samples/simple-socket-io-server" }, "author": "Redhouane Sobaihi", "license": "ISC", "workspaces": [ - "samples/simple-engine-io-server" + "samples/simple-engine-io-server", + "samples/simple-socket-io-server" ] } diff --git a/samples/simple-engine-io-server/package-lock.json b/samples/simple-engine-io-server/package-lock.json deleted file mode 100644 index 40f75d1..0000000 --- a/samples/simple-engine-io-server/package-lock.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "name": "simple-server", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "simple-server", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "engine.io": "^6.5.4" - } - }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", - "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/samples/simple-socket-io-server/index.js b/samples/simple-socket-io-server/index.js new file mode 100644 index 0000000..fc3dad6 --- /dev/null +++ b/samples/simple-socket-io-server/index.js @@ -0,0 +1,18 @@ +const { Server } = require('socket.io'); + +const io = new Server({ + cors: { + origin: '*', + methods: ['GET', 'POST'] + }, +}) + + +io.on("connection", (socket) => { + // ... + socket.on('data', (data) => { + console.log(data); + }) +}); + +io.listen(process.env.PORT || 3000); diff --git a/samples/simple-socket-io-server/package.json b/samples/simple-socket-io-server/package.json new file mode 100644 index 0000000..6aa6a01 --- /dev/null +++ b/samples/simple-socket-io-server/package.json @@ -0,0 +1,15 @@ +{ + "name": "simple-socket-io-server", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "DEBUG=engine,socket.io* node index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "socket.io": "^4.8.0" + } +} From 429cec5f2e60a9e16ddb57adb4de56c4839bcd59 Mon Sep 17 00:00:00 2001 From: redouane Date: Tue, 1 Oct 2024 14:24:40 +0100 Subject: [PATCH 15/26] feat: add packets serialization and tests - add connect and disconnect packet construction logic with tests --- SocketIO.sln | 7 ++ src/SocketIO.Client/Packets/PacketData.cs | 8 +- src/SocketIO.Client/Packets/PacketType.cs | 83 +++++++++++++++---- tests/SocketIO.Client.Tests/GlobalUsings.cs | 1 + .../Packets/ConnectPacketTests.cs | 53 ++++++++++++ .../Packets/DiconnectPacketTests.cs | 56 +++++++++++++ .../SocketIO.Client.Tests.csproj | 29 +++++++ 7 files changed, 215 insertions(+), 22 deletions(-) create mode 100644 tests/SocketIO.Client.Tests/GlobalUsings.cs create mode 100644 tests/SocketIO.Client.Tests/Packets/ConnectPacketTests.cs create mode 100644 tests/SocketIO.Client.Tests/Packets/DiconnectPacketTests.cs create mode 100644 tests/SocketIO.Client.Tests/SocketIO.Client.Tests.csproj diff --git a/SocketIO.sln b/SocketIO.sln index d3b1c57..0397db5 100644 --- a/SocketIO.sln +++ b/SocketIO.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineIO.Client.Tests", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SocketIO.Client", "src\SocketIO.Client\SocketIO.Client.csproj", "{222889F5-6A36-4A8A-8C78-A910E40D4F82}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SocketIO.Client.Tests", "tests\SocketIO.Client.Tests\SocketIO.Client.Tests.csproj", "{E0C636E7-91B0-44AE-9752-F08326C7C5F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,11 +44,16 @@ Global {222889F5-6A36-4A8A-8C78-A910E40D4F82}.Debug|Any CPU.Build.0 = Debug|Any CPU {222889F5-6A36-4A8A-8C78-A910E40D4F82}.Release|Any CPU.ActiveCfg = Release|Any CPU {222889F5-6A36-4A8A-8C78-A910E40D4F82}.Release|Any CPU.Build.0 = Release|Any CPU + {E0C636E7-91B0-44AE-9752-F08326C7C5F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0C636E7-91B0-44AE-9752-F08326C7C5F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0C636E7-91B0-44AE-9752-F08326C7C5F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0C636E7-91B0-44AE-9752-F08326C7C5F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {D1C745FE-52AF-4145-8243-9FCA91BE7916} = {80C612B0-98E1-4E6B-AF10-5BB3087F0864} {4F6177C5-3234-4897-B61C-49C44D3E2EF8} = {577735D4-BC7E-464A-BC9B-93AD8B5B6738} {5D824F05-5793-4768-99DF-E4D993F16B75} = {D8B5DE1E-C83F-40A9-9D27-D2FBB3866F0A} {222889F5-6A36-4A8A-8C78-A910E40D4F82} = {577735D4-BC7E-464A-BC9B-93AD8B5B6738} + {E0C636E7-91B0-44AE-9752-F08326C7C5F2} = {D8B5DE1E-C83F-40A9-9D27-D2FBB3866F0A} EndGlobalSection EndGlobal diff --git a/src/SocketIO.Client/Packets/PacketData.cs b/src/SocketIO.Client/Packets/PacketData.cs index 3fed736..b232cf0 100644 --- a/src/SocketIO.Client/Packets/PacketData.cs +++ b/src/SocketIO.Client/Packets/PacketData.cs @@ -9,7 +9,7 @@ internal interface IPacketData void Serialize(Utf8JsonWriter stream); } -internal sealed class TextPacketData : PacketData +internal readonly struct TextPacketData : IPacketData { public string Data { get; } @@ -24,7 +24,7 @@ public void Serialize(Utf8JsonWriter stream) } } -internal sealed class JsonPacketData : PacketData where T : class +internal readonly struct JsonPacketData : IPacketData where T : class { public T Data { get; } @@ -39,7 +39,7 @@ public void Serialize(Utf8JsonWriter stream) } } -internal sealed class BinaryPacketData : PacketData +internal readonly struct BinaryPacketData : IPacketData { public BinaryPacketData(int id, ReadOnlyMemory data) { @@ -54,7 +54,7 @@ public BinaryPacketData(int id, ReadOnlyMemory data) public bool Placeholder => true; [JsonPropertyName("num")] - public int Id { get; set; } + public int Id { get; } public void Serialize(Utf8JsonWriter stream) { diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs index d37ad9b..4bcf3cd 100644 --- a/src/SocketIO.Client/Packets/PacketType.cs +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -57,14 +57,17 @@ public enum PacketType : byte public class Packet { - public static Packet ConnectPacket = new(PacketType.Connect); + public static readonly Packet ConnectPacket = new(PacketType.Connect); public static Packet DisconnectPacket = new(PacketType.Disconnect); + const string DefaultNamespace = "/"; + + const string DefaultEventName = "message"; + public PacketType Type { get; } - public string? Namespace { get; internal set; } + public string? Namespace { get; } public uint? AckId { get; } - public string Event { get; } private readonly List _data = new(); @@ -72,34 +75,68 @@ public class Packet private readonly MemoryStream _buffer = new(); public Packet(PacketType type) - : this(type, "/", "message") + : this(type, DefaultNamespace, DefaultEventName) + { + Type = type; + } + + public Packet(PacketType type, string @namespace) + : this(type, @namespace, DefaultEventName) { Type = type; - Namespace = "/"; + Namespace = @namespace; } public Packet(PacketType type, string @namespace, string @event) { + if ((type == PacketType.Connect || type == PacketType.Disconnect) && @event != DefaultEventName) + { + throw new ArgumentException(); + } + Type = type; Namespace = @namespace; - Event = @event; - this.AddItem(Event); + Event = @event!; + + if (type is PacketType.Event or PacketType.BinaryEvent) + { + this.AddItem(Event); + } } public Packet(PacketType type, string @namespace, string @event, uint ackId) : this(type, @namespace, @event) { + if (type != PacketType.Ack && type != PacketType.BinaryAck) + { + throw new ArgumentException(); + } + AckId = ackId; } + private void _AddItem(T data) where T : IPacketData + { + if (Type == PacketType.Connect || Type == PacketType.Disconnect) + { + throw new InvalidOperationException(); + } + + if (Type == PacketType.BinaryEvent || Type == PacketType.BinaryAck) + { + throw new InvalidOperationException(); + } + + this._data.Add(data); + } + /// /// Add plain text data to packet /// /// Plain text data public void AddItem(string data) { - // TODO: check if packet type is not binary event, if it is, we should throw - this._data.Add(new TextPacketData(data)); + this._AddItem(new TextPacketData(data)); } /// @@ -108,10 +145,8 @@ public void AddItem(string data) /// Binary data public void AddItem(ReadOnlyMemory data) { - // TODO: check if packet type is binary event. if not, we should throw - int id = this._data.OfType().Count(); - this._data.Add(new BinaryPacketData(id, data)); + this._AddItem(new BinaryPacketData(id, data)); } /// @@ -121,7 +156,7 @@ public void AddItem(ReadOnlyMemory data) /// Data type public void AddItem(T data) where T : class { - this._data.Add(new JsonPacketData(data)); + this._AddItem(new JsonPacketData(data)); } /// @@ -136,16 +171,23 @@ internal MemoryStream Serialize() // write packet type headerWriter.Write((char)this.Type); - if (Type is PacketType.BinaryEvent or PacketType.BinaryAck && this._data.Count > 0) + if (Type is PacketType.BinaryEvent || Type == PacketType.BinaryAck) { var count = this._data.OfType().Count(); - headerWriter.Write(count); - headerWriter.Write('-'); + if (count > 0) + { + headerWriter.Write(count); + headerWriter.Write('-'); + } } // write packet namespace - headerWriter.Write(this.Namespace); - headerWriter.Write(','); + if (Namespace != DefaultNamespace) + { + headerWriter.Write('/'); + headerWriter.Write(this.Namespace); + headerWriter.Write(','); + } // write acknowledgement id if set if (this.AckId.HasValue) @@ -155,6 +197,11 @@ internal MemoryStream Serialize() headerWriter.Flush(); // write packet content + if (Type == PacketType.Connect || Type == PacketType.Disconnect) + { + return this._buffer; + } + dataWriter.WriteStartArray(); foreach (IPacketData item in this._data) { diff --git a/tests/SocketIO.Client.Tests/GlobalUsings.cs b/tests/SocketIO.Client.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/SocketIO.Client.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/tests/SocketIO.Client.Tests/Packets/ConnectPacketTests.cs b/tests/SocketIO.Client.Tests/Packets/ConnectPacketTests.cs new file mode 100644 index 0000000..969d1b4 --- /dev/null +++ b/tests/SocketIO.Client.Tests/Packets/ConnectPacketTests.cs @@ -0,0 +1,53 @@ +using System.Text; + +using SocketIO.Client.Packets; + +namespace SocketIO.Client.Tests.Packets; + +public class ConnectPacketTests +{ + [Fact] + void ShouldCreateConnectPacket() + { + var packet = Packet.ConnectPacket; + Assert.Equal(PacketType.Connect, packet.Type); + Assert.Equal("/", packet.Namespace); + } + + [Fact] + void ShouldCreateConnectPacketWithNamespace() + { + var @namespace = "test"; + + var packet = new Packet(PacketType.Connect, @namespace); + + Assert.Equal(PacketType.Connect, packet.Type); + Assert.Equal(@namespace, packet.Namespace); + } + + [Fact] + void ShouldSerializeConnectPacket() + { + var connectPacket = Packet.ConnectPacket; + + var serialized = connectPacket.Serialize(); + var plaintext = Encoding.UTF8.GetString(serialized.ToArray()); + + Assert.Equal(PacketType.Connect, connectPacket.Type); + Assert.Equal("0", plaintext); + } + + [Fact] + void ShouldSerializeConnectPacketWithNamespace() + { + var @namespace = "test"; + var connectPacket = new Packet(PacketType.Connect, @namespace);; + + var serialized = connectPacket.Serialize(); + var plaintext = Encoding.UTF8.GetString(serialized.ToArray()); + + Assert.Equal(PacketType.Connect, connectPacket.Type); + Assert.Equal($"0/{@namespace},", plaintext); + } + +} \ No newline at end of file diff --git a/tests/SocketIO.Client.Tests/Packets/DiconnectPacketTests.cs b/tests/SocketIO.Client.Tests/Packets/DiconnectPacketTests.cs new file mode 100644 index 0000000..b4048ef --- /dev/null +++ b/tests/SocketIO.Client.Tests/Packets/DiconnectPacketTests.cs @@ -0,0 +1,56 @@ +using System.Text; + +using SocketIO.Client.Packets; + +namespace SocketIO.Client.Tests.Packets; + +public class DisconnectPacketTests +{ + [Fact] + void ShouldCreateDisconnectPacket() + { + var packet = Packet.DisconnectPacket; + + Assert.Equal(PacketType.Disconnect, packet.Type); + Assert.Equal("/", packet.Namespace); + } + + [Fact] + void ShouldCreateDisconnectPacketWithNamespace() + { + var @namespace = "test"; + var packet = new Packet(PacketType.Disconnect, @namespace); + + Assert.Equal(PacketType.Disconnect, packet.Type); + Assert.Equal(@namespace, packet.Namespace); + } + + [Fact] + void ShouldSerializeDisconnectPacket() + { + var connectPacket = Packet.DisconnectPacket; + + var serialized = connectPacket.Serialize(); + var encodedPacket = Encoding.UTF8.GetString(serialized.ToArray()); + + string expectedEncodedPacket = "1"; + + Assert.Equal(PacketType.Disconnect, connectPacket.Type); + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + + [Fact] + void ShouldSerializeDisconnectPacketWithNamespace() + { + var @namespace = "test"; + var packet = new Packet(PacketType.Disconnect, @namespace);; + + var serialized = packet.Serialize(); + var encodedPacket = Encoding.UTF8.GetString(serialized.ToArray()); + + string expectedEncodedPacket = $"1/{@namespace},"; + + Assert.Equal(PacketType.Disconnect, packet.Type); + Assert.Equal(expectedEncodedPacket, encodedPacket); + } +} \ No newline at end of file diff --git a/tests/SocketIO.Client.Tests/SocketIO.Client.Tests.csproj b/tests/SocketIO.Client.Tests/SocketIO.Client.Tests.csproj new file mode 100644 index 0000000..0aa2046 --- /dev/null +++ b/tests/SocketIO.Client.Tests/SocketIO.Client.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + SocketIO.Client.Tests + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From 4aedc027164baa1b40913afcc1b92044eecb47e7 Mon Sep 17 00:00:00 2001 From: redouane Date: Tue, 1 Oct 2024 14:36:47 +0100 Subject: [PATCH 16/26] chore: make fields nullable --- src/EngineIO.Client/ClientOptions.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/EngineIO.Client/ClientOptions.cs b/src/EngineIO.Client/ClientOptions.cs index 789c965..88db4bb 100644 --- a/src/EngineIO.Client/ClientOptions.cs +++ b/src/EngineIO.Client/ClientOptions.cs @@ -5,7 +5,12 @@ public class ClientOptions /// /// Engine.io server Uri. /// - public string BaseAddress { get; set; } = null!; + public string? BaseAddress { get; set; } + + /// + /// Engine.io path, default to "engine.io" + /// + public string? Path { get; set; } /// /// Flag indicating whether client should automatically update from HTTP polling to websocket transport. From 977654942b5663760d5d22f60701385dae82394e Mon Sep 17 00:00:00 2001 From: redouane Date: Tue, 1 Oct 2024 14:37:21 +0100 Subject: [PATCH 17/26] feat: add `Path` parameter and refactor `Send` methods --- src/SocketIO.Client/IO.cs | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/SocketIO.Client/IO.cs b/src/SocketIO.Client/IO.cs index 5deb907..0893fd4 100644 --- a/src/SocketIO.Client/IO.cs +++ b/src/SocketIO.Client/IO.cs @@ -5,15 +5,22 @@ using System.Threading.Tasks; using EngineIO.Client; +using EngineIO.Client.Transports; +using PacketFormat = EngineIO.Client.Packets.PacketFormat; using SocketIO.Client.Packets; + namespace SocketIO.Client; public class IO { + private static readonly string DefaultPath = "socket.io"; + private readonly Engine _client; private readonly string _baseUrl; + + public string Path { get; set; } = DefaultPath; // Map namespace with its corresponding sid private readonly Dictionary _namespaces = new(); @@ -26,7 +33,7 @@ public IO(string baseUrl) { config.BaseAddress = baseUrl; config.AutoUpgrade = true; - + config.Path = Path; // TODO: allow passing custom headers and queries }); } @@ -39,24 +46,38 @@ public Task Connect(string? @namespace = default) public async IAsyncEnumerable> ListenAsync( string? @namespace = default, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - yield return await Task.FromResult(Array.Empty()); + // TODO: + yield return Array.Empty(); + } + + private Task _Send(Packet packet) + { + var buffer = packet.Serialize().ToArray(); + var eioPacker = EngineIO.Client.Packets.Packet.CreatePacket(PacketFormat.PlainText, buffer); + + return this._client.SendAsync(eioPacker.ToWirePacket()); } public Task SendAsync(ReadOnlyMemory data, string? @namespace = null) { - var packet = new Packet(PacketType.Event) { Namespace = @namespace, }; + var packet = new Packet(PacketType.BinaryEvent, @namespace); packet.AddItem(data); // TODO: - return Task.CompletedTask; + return this._Send(packet); + } + + public Task SendAsync(string text, string? @namespace = null) + { + var packet = new Packet(PacketType.Event, @namespace); + packet.AddItem(text); + return this._Send(packet); } public Task SendAsync(T data, string? @namespace = null) where T : class { - // TODO: send as string data - var packet = new Packet(PacketType.Event) { Namespace = @namespace, }; + var packet = new Packet(PacketType.Event, @namespace); packet.AddItem(data); - - return Task.CompletedTask; + return this._Send(packet); } } \ No newline at end of file From b5128755b3f0be913f24fce0ef8ec5e6e3d4973d Mon Sep 17 00:00:00 2001 From: redouane Date: Tue, 1 Oct 2024 15:45:41 +0100 Subject: [PATCH 18/26] feat: add more packet construction tests and fix namespace handling --- src/SocketIO.Client/Packets/PacketType.cs | 4 +- .../Packets/EventPacketTests.cs | 134 ++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs index 4bcf3cd..681ce5d 100644 --- a/src/SocketIO.Client/Packets/PacketType.cs +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -87,7 +87,7 @@ public Packet(PacketType type, string @namespace) Namespace = @namespace; } - public Packet(PacketType type, string @namespace, string @event) + public Packet(PacketType type, string? @namespace, string @event) { if ((type == PacketType.Connect || type == PacketType.Disconnect) && @event != DefaultEventName) { @@ -95,7 +95,7 @@ public Packet(PacketType type, string @namespace, string @event) } Type = type; - Namespace = @namespace; + Namespace = @namespace ?? DefaultNamespace; Event = @event!; if (type is PacketType.Event or PacketType.BinaryEvent) diff --git a/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs b/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs new file mode 100644 index 0000000..3da2c47 --- /dev/null +++ b/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs @@ -0,0 +1,134 @@ +using System.Text; + +using SocketIO.Client.Packets; + +namespace SocketIO.Client.Tests.Packets; + +// class used for packet with json payload tests +class Foo +{ + public string? Value { get; set; } +} + + +public class EventPacketTests +{ + [Fact] + void ShouldCreateEventPacket() + { + var packet = new Packet(PacketType.Event); + + string expectedDefaultNamespace = "/"; + string expectedDefaultEvent = "message"; + + Assert.Equal(PacketType.Event, packet.Type); + Assert.Equal(expectedDefaultNamespace, packet.Namespace); + Assert.Equal(expectedDefaultEvent, packet.Event); + } + + [Fact] + void ShouldCreateEventPacketWithNamespace() + { + var @namespace = "test"; + var packet = new Packet(PacketType.Event, "test"); + + var expectedNamespace = @namespace; + var expectedDefaultEvent = "message"; + + Assert.Equal(PacketType.Event, packet.Type); + Assert.Equal(expectedNamespace, packet.Namespace); + Assert.Equal(expectedDefaultEvent, packet.Event); + } + + [Fact] + void ShouldCreateEventPacketWithEventName() + { + var eventName = "test"; + var packet = new Packet(PacketType.Event, null, eventName); + + var expectedDefaultNamespace = "/"; + var expectedDefaultEvent = eventName; + + Assert.Equal(PacketType.Event, packet.Type); + Assert.Equal(expectedDefaultNamespace, packet.Namespace); + Assert.Equal(expectedDefaultEvent, packet.Event); + } + + [Fact] + void ShouldSerializePlainTextEventPacket() + { + var packet = new Packet(PacketType.Event); + packet.AddItem("Hello!"); + var encodedPacket = Encoding.UTF8.GetString(packet.Serialize().ToArray()); + + string expectedEncodedPacket = """2["message","Hello!"]"""; + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + + [Fact] + void ShouldSerializePlainTextEventWithNamespace() + { + var @namespace = "test"; + var expectedEncodedPacket = $"""2/{@namespace},["message","Hello!"]"""; + + var packet = new Packet(PacketType.Event, @namespace); + packet.AddItem("Hello!"); + var encodedPacket = Encoding.UTF8.GetString(packet.Serialize().ToArray()); + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + + [Fact] + void ShouldSerializePlainTextEventWithEventName() + { + var eventName = "test"; + var expectedEncodedPacket = $$"""2["{{eventName}}","Hello!"]"""; + + var packet = new Packet(PacketType.Event, null, eventName); + packet.AddItem("Hello!"); + var encodedPacket = Encoding.UTF8.GetString(packet.Serialize().ToArray()); + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + + [Fact] + void ShouldSerializeJsonEventPacket() + { + var packet = new Packet(PacketType.Event); + packet.AddItem(new Foo { Value = "bar" }); + var encodedPacket = Encoding.UTF8.GetString(packet.Serialize().ToArray()); + + string expectedEncodedPacket = """2["message",{"Value":"bar"}]"""; + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + + [Fact] + void ShouldSerializeJsonEventPacketWithNamespace() + { + var @namespace = "test"; + var expectedEncodedPacket = $$"""2/{{@namespace}},["message",{"Value":"bar"}]"""; + + var packet = new Packet(PacketType.Event, @namespace); + packet.AddItem(new Foo { Value = "bar" }); + var encodedPacket = Encoding.UTF8.GetString(packet.Serialize().ToArray()); + + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + + [Fact] + void ShouldSerializeJsonEventWithEventName() + { + var eventName = "test"; + var expectedEncodedPacket = $$"""2["{{eventName}}",{"Value":"bar"}]"""; + + var packet = new Packet(PacketType.Event, null, eventName); + packet.AddItem(new Foo { Value = "bar" }); + + var encodedPacket = Encoding.UTF8.GetString(packet.Serialize().ToArray()); + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } +} \ No newline at end of file From c515caee414f20d9d9ea407c7517a490e1f94e35 Mon Sep 17 00:00:00 2001 From: redouane Date: Tue, 1 Oct 2024 15:46:14 +0100 Subject: [PATCH 19/26] feat: add negative test cases --- tests/SocketIO.Client.Tests/Packets/ConnectPacketTests.cs | 8 +++++++- .../SocketIO.Client.Tests/Packets/DiconnectPacketTests.cs | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/SocketIO.Client.Tests/Packets/ConnectPacketTests.cs b/tests/SocketIO.Client.Tests/Packets/ConnectPacketTests.cs index 969d1b4..a83fa14 100644 --- a/tests/SocketIO.Client.Tests/Packets/ConnectPacketTests.cs +++ b/tests/SocketIO.Client.Tests/Packets/ConnectPacketTests.cs @@ -49,5 +49,11 @@ void ShouldSerializeConnectPacketWithNamespace() Assert.Equal(PacketType.Connect, connectPacket.Type); Assert.Equal($"0/{@namespace},", plaintext); } - + + [Fact] + void ShouldThrowExceptionWhenAddingItemToConnectPacket() + { + var packet = Packet.ConnectPacket; + Assert.Throws(() => packet.AddItem("World")); + } } \ No newline at end of file diff --git a/tests/SocketIO.Client.Tests/Packets/DiconnectPacketTests.cs b/tests/SocketIO.Client.Tests/Packets/DiconnectPacketTests.cs index b4048ef..3b2cb3f 100644 --- a/tests/SocketIO.Client.Tests/Packets/DiconnectPacketTests.cs +++ b/tests/SocketIO.Client.Tests/Packets/DiconnectPacketTests.cs @@ -53,4 +53,11 @@ void ShouldSerializeDisconnectPacketWithNamespace() Assert.Equal(PacketType.Disconnect, packet.Type); Assert.Equal(expectedEncodedPacket, encodedPacket); } + + [Fact] + void ShouldThrowExceptionWhenAddingItemToDisonnectPacket() + { + var packet = Packet.DisconnectPacket; + Assert.Throws(() => packet.AddItem("World")); + } } \ No newline at end of file From 7efb85065ed5e2ebf66d55c862c59497b78b023c Mon Sep 17 00:00:00 2001 From: redouane Date: Tue, 1 Oct 2024 15:54:37 +0100 Subject: [PATCH 20/26] fix: fix binary payload check and add negative test cases --- src/SocketIO.Client/Packets/PacketType.cs | 20 ++++++++++++++----- .../Packets/EventPacketTests.cs | 8 ++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs index 681ce5d..332dc47 100644 --- a/src/SocketIO.Client/Packets/PacketType.cs +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -122,11 +122,6 @@ private void _AddItem(T data) where T : IPacketData throw new InvalidOperationException(); } - if (Type == PacketType.BinaryEvent || Type == PacketType.BinaryAck) - { - throw new InvalidOperationException(); - } - this._data.Add(data); } @@ -136,6 +131,11 @@ private void _AddItem(T data) where T : IPacketData /// Plain text data public void AddItem(string data) { + if (Type == PacketType.BinaryEvent || Type == PacketType.BinaryAck) + { + throw new InvalidOperationException(); + } + this._AddItem(new TextPacketData(data)); } @@ -145,6 +145,11 @@ public void AddItem(string data) /// Binary data public void AddItem(ReadOnlyMemory data) { + if (Type == PacketType.Event || Type == PacketType.Ack) + { + throw new InvalidOperationException(); + } + int id = this._data.OfType().Count(); this._AddItem(new BinaryPacketData(id, data)); } @@ -156,6 +161,11 @@ public void AddItem(ReadOnlyMemory data) /// Data type public void AddItem(T data) where T : class { + if (Type == PacketType.BinaryEvent || Type == PacketType.BinaryAck) + { + throw new InvalidOperationException(); + } + this._AddItem(new JsonPacketData(data)); } diff --git a/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs b/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs index 3da2c47..77754be 100644 --- a/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs +++ b/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs @@ -65,6 +65,14 @@ void ShouldSerializePlainTextEventPacket() Assert.Equal(expectedEncodedPacket, encodedPacket); } + + [Fact] + void ShouldThrowWhenAddingInvalidPayloadPacket() + { + var packet = new Packet(PacketType.Event); + var invalidPayload = new ReadOnlyMemory(new byte[] { 1, 2, 3 }); + Assert.Throws(() => packet.AddItem(invalidPayload)); + } [Fact] void ShouldSerializePlainTextEventWithNamespace() From 9014e8eb167f9125ed7b0a9801c0665e953c669e Mon Sep 17 00:00:00 2001 From: redouane Date: Tue, 1 Oct 2024 15:55:36 +0100 Subject: [PATCH 21/26] fix: set default event name and add ack response --- samples/simple-socket-io-server/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/simple-socket-io-server/index.js b/samples/simple-socket-io-server/index.js index fc3dad6..bc72d46 100644 --- a/samples/simple-socket-io-server/index.js +++ b/samples/simple-socket-io-server/index.js @@ -10,8 +10,9 @@ const io = new Server({ io.on("connection", (socket) => { // ... - socket.on('data', (data) => { + socket.on('message', (data, callback) => { console.log(data); + callback?.apply(this,['OK']); }) }); From 9665e0276de5b2f5366403216d0e24d9cfe6afc2 Mon Sep 17 00:00:00 2001 From: redouane Date: Tue, 1 Oct 2024 18:06:52 +0100 Subject: [PATCH 22/26] fix: check packet type on `AddItem` for binary data --- src/SocketIO.Client/Packets/PacketType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs index 332dc47..0710d6a 100644 --- a/src/SocketIO.Client/Packets/PacketType.cs +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -145,7 +145,7 @@ public void AddItem(string data) /// Binary data public void AddItem(ReadOnlyMemory data) { - if (Type == PacketType.Event || Type == PacketType.Ack) + if (Type != PacketType.BinaryEvent || Type != PacketType.BinaryAck) { throw new InvalidOperationException(); } From a5932207071fd4bc0ff661fa5118dbcedd85d93a Mon Sep 17 00:00:00 2001 From: redouane Date: Wed, 2 Oct 2024 13:34:43 +0100 Subject: [PATCH 23/26] fix: set default event value, update Binary packet construction logic - add comments - set `DefaultEventName` as default event name - fix Binary and BinaryAck packet constructor --- src/SocketIO.Client/Packets/PacketType.cs | 27 +++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs index 0710d6a..ed60799 100644 --- a/src/SocketIO.Client/Packets/PacketType.cs +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -67,7 +67,7 @@ public class Packet public PacketType Type { get; } public string? Namespace { get; } - public uint? AckId { get; } + public int? AckId { get; } public string Event { get; } private readonly List _data = new(); @@ -80,31 +80,31 @@ public Packet(PacketType type) Type = type; } - public Packet(PacketType type, string @namespace) + public Packet(PacketType type, string? @namespace) : this(type, @namespace, DefaultEventName) { Type = type; Namespace = @namespace; } - public Packet(PacketType type, string? @namespace, string @event) + public Packet(PacketType type, string? @namespace, string? @event) { - if ((type == PacketType.Connect || type == PacketType.Disconnect) && @event != DefaultEventName) + if ((type is PacketType.Connect or PacketType.Disconnect) && @event != DefaultEventName) { throw new ArgumentException(); } Type = type; Namespace = @namespace ?? DefaultNamespace; - Event = @event!; + Event = @event ?? DefaultEventName; - if (type is PacketType.Event or PacketType.BinaryEvent) + if (type is PacketType.Event or PacketType.Ack or PacketType.BinaryEvent or PacketType.BinaryAck) { - this.AddItem(Event); + this._AddItem(new TextPacketData(Event)); } } - public Packet(PacketType type, string @namespace, string @event, uint ackId) + public Packet(PacketType type, int ackId, string? @namespace, string? @event) : this(type, @namespace, @event) { if (type != PacketType.Ack && type != PacketType.BinaryAck) @@ -145,7 +145,7 @@ public void AddItem(string data) /// Binary data public void AddItem(ReadOnlyMemory data) { - if (Type != PacketType.BinaryEvent || Type != PacketType.BinaryAck) + if (Type != PacketType.BinaryEvent && Type != PacketType.BinaryAck) { throw new InvalidOperationException(); } @@ -195,18 +195,21 @@ internal MemoryStream Serialize() if (Namespace != DefaultNamespace) { headerWriter.Write('/'); - headerWriter.Write(this.Namespace); + headerWriter.Write(Namespace); headerWriter.Write(','); } // write acknowledgement id if set - if (this.AckId.HasValue) + if (AckId.HasValue) { - headerWriter.Write(this.AckId); + headerWriter.Write(AckId); } headerWriter.Flush(); // write packet content + + // Connect and Disconnect packet do not contain payload + // therefore, writing packet payload is skipped if (Type == PacketType.Connect || Type == PacketType.Disconnect) { return this._buffer; From 27cd3129499e2ed9534c7e25150d239bc49b1465 Mon Sep 17 00:00:00 2001 From: redouane Date: Wed, 2 Oct 2024 13:35:04 +0100 Subject: [PATCH 24/26] test: add Ack packet tests --- .../Packets/EventPacketTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs b/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs index 77754be..d7145bc 100644 --- a/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs +++ b/tests/SocketIO.Client.Tests/Packets/EventPacketTests.cs @@ -53,6 +53,18 @@ void ShouldCreateEventPacketWithEventName() Assert.Equal(expectedDefaultNamespace, packet.Namespace); Assert.Equal(expectedDefaultEvent, packet.Event); } + + [Fact] + void ShouldCreateEventPacketWithAckId() + { + var ackId = 42; + var packet = new Packet(PacketType.Ack, ackId, null, null); + + var expectedAckId = ackId; + + Assert.Equal(PacketType.Ack, packet.Type); + Assert.Equal(expectedAckId, packet.AckId); + } [Fact] void ShouldSerializePlainTextEventPacket() @@ -100,6 +112,19 @@ void ShouldSerializePlainTextEventWithEventName() Assert.Equal(expectedEncodedPacket, encodedPacket); } + [Fact] + void ShouldSerializePlainTextWithAckIdPacket() + { + var ackId = 42; + var expectedEncodedPacket = $$"""3{{ackId}}["message","Hello!"]"""; + + var packet = new Packet(PacketType.Ack, ackId, null, null); + packet.AddItem("Hello!"); + var encodedPacket = Encoding.UTF8.GetString(packet.Serialize().ToArray()); + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + [Fact] void ShouldSerializeJsonEventPacket() { @@ -139,4 +164,17 @@ void ShouldSerializeJsonEventWithEventName() Assert.Equal(expectedEncodedPacket, encodedPacket); } + + [Fact] + void ShouldSerializeJsonEventWithAckIdPacket() + { + var ackId = 42; + var expectedEncodedPacket = $$"""3{{ackId}}["message",{"Value":"bar"}]"""; + + var packet = new Packet(PacketType.Ack, ackId, null, null); + packet.AddItem(new Foo { Value = "bar" }); + var encodedPacket = Encoding.UTF8.GetString(packet.Serialize().ToArray()); + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } } \ No newline at end of file From 30926908b5879fffded7c1fae4a8948baadad9c5 Mon Sep 17 00:00:00 2001 From: redouane Date: Wed, 2 Oct 2024 13:35:16 +0100 Subject: [PATCH 25/26] test: add binary events tests --- .../Packets/BinaryEventPacketTests.cs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/SocketIO.Client.Tests/Packets/BinaryEventPacketTests.cs diff --git a/tests/SocketIO.Client.Tests/Packets/BinaryEventPacketTests.cs b/tests/SocketIO.Client.Tests/Packets/BinaryEventPacketTests.cs new file mode 100644 index 0000000..f73f29c --- /dev/null +++ b/tests/SocketIO.Client.Tests/Packets/BinaryEventPacketTests.cs @@ -0,0 +1,109 @@ +using System.Text; + +using SocketIO.Client.Packets; + +namespace SocketIO.Client.Tests.Packets; + +public class BinaryEventPacketTests +{ + [Fact] + void ShouldCreateBinaryEventPacket() + { + var packetType = PacketType.BinaryEvent; + var expectedPacketType = packetType; + var expectedDefaultNamespace = "/"; + + var packet = new Packet(packetType); + packet.AddItem(new ReadOnlyMemory([ 1, 2, 3 ])); + + Assert.Equal(expectedPacketType, packet.Type); + Assert.Equal(expectedDefaultNamespace, packet.Namespace); + } + + [Fact] + public void ShouldCreateBinaryPacketWithNamespace() + { + var @namespace = "test"; + var expectedNamespace = @namespace; + + var packet = new Packet(PacketType.BinaryEvent, @namespace); + + Assert.Equal(expectedNamespace, packet.Namespace); + } + + [Fact] + public void ShouldCreateBinaryPacketWithEventName() + { + var eventName = "test"; + var expectedEventName = eventName; + + var packet = new Packet(PacketType.BinaryEvent, null, eventName); + + Assert.Equal(expectedEventName, packet.Event); + } + + [Fact] + public void ShouldCreateBinaryPacketWithAckId() + { + var ackId = 42; + var expectedAckId = ackId; + + var packet = new Packet(PacketType.BinaryAck, ackId, null, null); + + Assert.Equal(expectedAckId, packet.AckId); + } + + [Fact] + public void ShouldSerializeBinaryEventPacket() + { + var expectedEncodedPacket = $$"""51-["message",{"_placeholder":true,"num":0}]"""; + var packet = new Packet(PacketType.BinaryEvent); + packet.AddItem(new ReadOnlyMemory([ 1, 2, 3 ])); + + var encodedPacket = Encoding.UTF8.GetString( packet.Serialize().ToArray()); + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + + [Fact] + public void ShouldSerializeBinaryPacketWithNamespace() + { + var @namespace = "test"; + var expectedEncodedPacket = $$"""51-/{{@namespace}},["message",{"_placeholder":true,"num":0}]"""; + + var packet = new Packet(PacketType.BinaryEvent, @namespace); + packet.AddItem(new ReadOnlyMemory([ 1, 2, 3 ])); + + var encodedPacket = Encoding.UTF8.GetString( packet.Serialize().ToArray()); + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + + [Fact] + public void ShouldSerializeBinaryPacketWithEventName() + { + var eventName = "test"; + var expectedEncodedPacket = $$"""51-["{{eventName}}",{"_placeholder":true,"num":0}]"""; + + var packet = new Packet(PacketType.BinaryEvent, null, eventName); + packet.AddItem(new ReadOnlyMemory([ 1, 2, 3 ])); + + var encodedPacket = Encoding.UTF8.GetString( packet.Serialize().ToArray()); + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } + + [Fact] + public void ShouldSerializeBinaryPacketWithAckId() + { + var ackId = 42; + var expectedEncodedPacket = $$"""61-42["message",{"_placeholder":true,"num":0}]"""; + + var packet = new Packet(PacketType.BinaryAck, ackId, null, null); + packet.AddItem(new ReadOnlyMemory([ 1, 2, 3 ])); + + var encodedPacket = Encoding.UTF8.GetString( packet.Serialize().ToArray()); + + Assert.Equal(expectedEncodedPacket, encodedPacket); + } +} \ No newline at end of file From c3830eedf497b013ff063dd1b4501f7c1603b77a Mon Sep 17 00:00:00 2001 From: redouane Date: Wed, 2 Oct 2024 13:51:24 +0100 Subject: [PATCH 26/26] chore: make IPacketData impls classes and make `Event` property nullable --- src/SocketIO.Client/Packets/PacketData.cs | 6 +++--- src/SocketIO.Client/Packets/PacketType.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SocketIO.Client/Packets/PacketData.cs b/src/SocketIO.Client/Packets/PacketData.cs index b232cf0..babc55a 100644 --- a/src/SocketIO.Client/Packets/PacketData.cs +++ b/src/SocketIO.Client/Packets/PacketData.cs @@ -9,7 +9,7 @@ internal interface IPacketData void Serialize(Utf8JsonWriter stream); } -internal readonly struct TextPacketData : IPacketData +internal sealed class TextPacketData : IPacketData { public string Data { get; } @@ -24,7 +24,7 @@ public void Serialize(Utf8JsonWriter stream) } } -internal readonly struct JsonPacketData : IPacketData where T : class +internal sealed class JsonPacketData : IPacketData where T : class { public T Data { get; } @@ -39,7 +39,7 @@ public void Serialize(Utf8JsonWriter stream) } } -internal readonly struct BinaryPacketData : IPacketData +internal sealed class BinaryPacketData : IPacketData { public BinaryPacketData(int id, ReadOnlyMemory data) { diff --git a/src/SocketIO.Client/Packets/PacketType.cs b/src/SocketIO.Client/Packets/PacketType.cs index ed60799..28d5867 100644 --- a/src/SocketIO.Client/Packets/PacketType.cs +++ b/src/SocketIO.Client/Packets/PacketType.cs @@ -68,7 +68,7 @@ public class Packet public PacketType Type { get; } public string? Namespace { get; } public int? AckId { get; } - public string Event { get; } + public string? Event { get; } private readonly List _data = new();