diff --git a/BootstrapBlazor.Extensions.sln b/BootstrapBlazor.Extensions.sln index 2850f3cb..2af8a1dd 100644 --- a/BootstrapBlazor.Extensions.sln +++ b/BootstrapBlazor.Extensions.sln @@ -156,8 +156,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.Mermaid", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.MeiliSearch", "src\components\BootstrapBlazor.MeiliSearch\BootstrapBlazor.MeiliSearch.csproj", "{4B086A62-5F5A-47BC-921F-35803F26DD68}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniTestSvgIcon", "test\UniTestSvgIcon\UniTestSvgIcon.csproj", "{F965576B-A801-4473-85FE-E100125FDEF5}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.RDKit", "src\components\BootstrapBlazor.RDKit\BootstrapBlazor.RDKit.csproj", "{7328E464-AE3C-4277-BEC3-422C56637066}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.SmilesDrawer", "src\components\BootstrapBlazor.SmilesDrawer\BootstrapBlazor.SmilesDrawer.csproj", "{84823875-1B07-4CCE-A009-29AEF90C6C10}" @@ -190,6 +188,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.Vditor", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.OfficeViewer", "src\components\BootstrapBlazor.OfficeViewer\BootstrapBlazor.OfficeViewer.csproj", "{2436940C-5920-D801-8A81-721F4C20A355}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.Socket", "src\extensions\BootstrapBlazor.Socket\BootstrapBlazor.Socket.csproj", "{965F1512-57DC-4621-9C74-E059A14BB866}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.TcpSocket", "src\extensions\BootstrapBlazor.TcpSocket\BootstrapBlazor.TcpSocket.csproj", "{3F7C6E3F-5AC2-4B13-A57F-9329E34C1F5C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestTcpSocket", "test\UnitTestTcpSocket\UnitTestTcpSocket.csproj", "{10D35EE5-FA31-4C80-B113-CD7A0FB76B4E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestSvgIcon", "test\UnitTestSvgIcon\UnitTestSvgIcon.csproj", "{7CAD5915-CE3E-31ED-B1AC-15C61C3ED8C3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -448,10 +454,6 @@ Global {4B086A62-5F5A-47BC-921F-35803F26DD68}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B086A62-5F5A-47BC-921F-35803F26DD68}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B086A62-5F5A-47BC-921F-35803F26DD68}.Release|Any CPU.Build.0 = Release|Any CPU - {F965576B-A801-4473-85FE-E100125FDEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F965576B-A801-4473-85FE-E100125FDEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F965576B-A801-4473-85FE-E100125FDEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F965576B-A801-4473-85FE-E100125FDEF5}.Release|Any CPU.Build.0 = Release|Any CPU {7328E464-AE3C-4277-BEC3-422C56637066}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7328E464-AE3C-4277-BEC3-422C56637066}.Debug|Any CPU.Build.0 = Debug|Any CPU {7328E464-AE3C-4277-BEC3-422C56637066}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -516,6 +518,22 @@ Global {2436940C-5920-D801-8A81-721F4C20A355}.Debug|Any CPU.Build.0 = Debug|Any CPU {2436940C-5920-D801-8A81-721F4C20A355}.Release|Any CPU.ActiveCfg = Release|Any CPU {2436940C-5920-D801-8A81-721F4C20A355}.Release|Any CPU.Build.0 = Release|Any CPU + {965F1512-57DC-4621-9C74-E059A14BB866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {965F1512-57DC-4621-9C74-E059A14BB866}.Debug|Any CPU.Build.0 = Debug|Any CPU + {965F1512-57DC-4621-9C74-E059A14BB866}.Release|Any CPU.ActiveCfg = Release|Any CPU + {965F1512-57DC-4621-9C74-E059A14BB866}.Release|Any CPU.Build.0 = Release|Any CPU + {3F7C6E3F-5AC2-4B13-A57F-9329E34C1F5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F7C6E3F-5AC2-4B13-A57F-9329E34C1F5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F7C6E3F-5AC2-4B13-A57F-9329E34C1F5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F7C6E3F-5AC2-4B13-A57F-9329E34C1F5C}.Release|Any CPU.Build.0 = Release|Any CPU + {10D35EE5-FA31-4C80-B113-CD7A0FB76B4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10D35EE5-FA31-4C80-B113-CD7A0FB76B4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10D35EE5-FA31-4C80-B113-CD7A0FB76B4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10D35EE5-FA31-4C80-B113-CD7A0FB76B4E}.Release|Any CPU.Build.0 = Release|Any CPU + {7CAD5915-CE3E-31ED-B1AC-15C61C3ED8C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CAD5915-CE3E-31ED-B1AC-15C61C3ED8C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CAD5915-CE3E-31ED-B1AC-15C61C3ED8C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CAD5915-CE3E-31ED-B1AC-15C61C3ED8C3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -588,7 +606,6 @@ Global {29E47F4A-CE03-42B5-BDAA-FB4B40D4C897} = {FF1089BE-C704-4374-B629-C57C08E1798F} {DA2198E1-2CA9-EE53-926B-7950AB4B5EBF} = {FF1089BE-C704-4374-B629-C57C08E1798F} {4B086A62-5F5A-47BC-921F-35803F26DD68} = {FF1089BE-C704-4374-B629-C57C08E1798F} - {F965576B-A801-4473-85FE-E100125FDEF5} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} {7328E464-AE3C-4277-BEC3-422C56637066} = {FF1089BE-C704-4374-B629-C57C08E1798F} {84823875-1B07-4CCE-A009-29AEF90C6C10} = {FF1089BE-C704-4374-B629-C57C08E1798F} {AA4EDA37-1D81-4235-A7F6-F1C112B364EF} = {FF1089BE-C704-4374-B629-C57C08E1798F} @@ -605,6 +622,10 @@ Global {4757B038-70E4-40B0-9B73-700EE5632B07} = {FF1089BE-C704-4374-B629-C57C08E1798F} {D417E1B9-D146-4983-81D0-79F3193B322B} = {FF1089BE-C704-4374-B629-C57C08E1798F} {2436940C-5920-D801-8A81-721F4C20A355} = {FF1089BE-C704-4374-B629-C57C08E1798F} + {965F1512-57DC-4621-9C74-E059A14BB866} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} + {3F7C6E3F-5AC2-4B13-A57F-9329E34C1F5C} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} + {10D35EE5-FA31-4C80-B113-CD7A0FB76B4E} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} + {7CAD5915-CE3E-31ED-B1AC-15C61C3ED8C3} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF} diff --git a/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj b/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj new file mode 100644 index 00000000..284ada07 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj @@ -0,0 +1,18 @@ + + + + 9.0.0-beta01 + + + + BootstrapBlazor Socket + BootstrapBlazor extensions of Socket + + + + + + + + + diff --git a/src/extensions/BootstrapBlazor.Socket/DataAdapter/DataPackageAdapter.cs b/src/extensions/BootstrapBlazor.Socket/DataAdapter/DataPackageAdapter.cs new file mode 100644 index 00000000..315994c2 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataAdapter/DataPackageAdapter.cs @@ -0,0 +1,80 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataAdapters; + +/// +/// Provides a base implementation for adapting data packages between different systems or formats. +/// +/// This abstract class serves as a foundation for implementing custom data package adapters. It defines +/// common methods for sending, receiving, and handling data packages, as well as a property for accessing the +/// associated data package handler. Derived classes should override the virtual methods to provide specific behavior +/// for handling data packages. +public class DataPackageAdapter : IDataPackageAdapter +{ + /// + /// + /// + public Func, ValueTask>? ReceivedCallBack { get; set; } + + /// + /// + /// + public IDataPackageHandler? DataPackageHandler { get; set; } + + /// + /// + /// + /// + /// + /// + public virtual async ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default) + { + if (DataPackageHandler != null) + { + if (DataPackageHandler.ReceivedCallBack == null) + { + DataPackageHandler.ReceivedCallBack = OnHandlerReceivedCallBack; + } + + // 如果存在数据处理器则调用其处理方法 + await DataPackageHandler.HandlerAsync(data, token); + } + } + + /// + /// + /// + /// + /// + /// + /// + public virtual bool TryConvertTo(ReadOnlyMemory data, IDataConverter socketDataConverter, out TEntity? entity) + { + entity = default; + var ret = socketDataConverter.TryConvertTo(data, out var v); + if (ret) + { + entity = v; + } + return ret; + } + + /// + /// Handles incoming data by invoking a callback method, if one is defined. + /// + /// This method is designed to be overridden in derived classes to provide custom handling of + /// incoming data. If a callback method is assigned, it will be invoked asynchronously with the provided + /// data. + /// The incoming data to be processed, represented as a read-only memory block of bytes. + /// + protected virtual async ValueTask OnHandlerReceivedCallBack(ReadOnlyMemory data) + { + if (ReceivedCallBack != null) + { + // 调用接收回调方法处理数据 + await ReceivedCallBack(data); + } + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataAdapter/IDataPackageAdapter.cs b/src/extensions/BootstrapBlazor.Socket/DataAdapter/IDataPackageAdapter.cs new file mode 100644 index 00000000..5e5a0268 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataAdapter/IDataPackageAdapter.cs @@ -0,0 +1,54 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataAdapters; + +/// +/// Defines an adapter for handling and transmitting data packages to a target destination. +/// +/// This interface provides methods for sending data asynchronously and configuring a data handler. +/// Implementations of this interface are responsible for managing the interaction between the caller and the underlying +/// data transmission mechanism. +public interface IDataPackageAdapter +{ + /// + /// Gets or sets the callback function to be invoked when data is received. + /// + /// The callback function is expected to handle the received data asynchronously. Ensure that the + /// implementation of the callback does not block the calling thread and completes promptly to avoid performance + /// issues. + Func, ValueTask>? ReceivedCallBack { get; set; } + + /// + /// Gets the handler responsible for processing data packages. + /// + IDataPackageHandler? DataPackageHandler { get; } + + /// + /// Asynchronously receives data from a source and processes it. + /// + /// This method does not return any result directly. It is intended for scenarios where data is received + /// and processed asynchronously. Ensure that the parameter contains valid data before calling + /// this method. + /// A read-only memory region containing the data to be received. The caller must ensure the memory is valid and + /// populated. + /// An optional cancellation token that can be used to cancel the operation. Defaults to if + /// not provided. + /// A representing the asynchronous operation. The task completes when the data has been + /// successfully received and processed. + ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default); + + /// + /// Attempts to convert the specified byte data into an entity of type . + /// + /// This method does not throw an exception if the conversion fails. Instead, it returns and sets to its default value. + /// The type of the entity to convert the data to. + /// The byte data to be converted. + /// The converter used to transform the byte data into an entity. + /// When this method returns, contains the converted entity if the conversion was successful; otherwise, the default + /// value for the type of the entity. + /// if the conversion was successful; otherwise, . + bool TryConvertTo(ReadOnlyMemory data, IDataConverter socketDataConverter, out TEntity? entity); +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataConverter/DataConverter.cs b/src/extensions/BootstrapBlazor.Socket/DataConverter/DataConverter.cs new file mode 100644 index 00000000..0a42c06e --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataConverter/DataConverter.cs @@ -0,0 +1,81 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Reflection; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Provides a base class for converting socket data into a specified entity type. +/// +/// The type of entity to convert the socket data into. +public class DataConverter(DataConverterCollections converters) : IDataConverter +{ + /// + /// 构造函数 + /// + public DataConverter() : this(new()) + { + + } + + /// + /// + /// + /// + /// + /// + public virtual bool TryConvertTo(ReadOnlyMemory data, [NotNullWhen(true)] out TEntity? entity) + { + var v = CreateEntity(); + var ret = Parse(data, v); + entity = ret ? v : default; + return ret; + } + + /// + /// 创建实体实例方法 + /// + /// + protected virtual TEntity CreateEntity() => Activator.CreateInstance(); + + /// + /// 将字节数据转换为指定实体类型的实例。 + /// + /// + /// + protected virtual bool Parse(ReadOnlyMemory data, TEntity entity) + { + // 使用 SocketDataPropertyAttribute 特性获取数据转换规则 + var ret = false; + if (entity != null) + { + var unuseProperties = new List(32); + + // 通过 SocketDataPropertyConverterAttribute 特性获取属性转换器 + var properties = entity.GetType().GetProperties().Where(p => p.CanWrite).ToList(); + foreach (var p in properties) + { + var attr = p.GetCustomAttribute(false) + ?? GetPropertyConverterAttribute(p); + if (attr != null) + { + p.SetValue(entity, attr.ConvertTo(data)); + } + } + ret = true; + } + return ret; + } + + private DataPropertyConverterAttribute? GetPropertyConverterAttribute(PropertyInfo propertyInfo) + { + DataPropertyConverterAttribute? attr = null; + if (converters.TryGetPropertyConverter(propertyInfo, out var v)) + { + attr = v; + } + return attr; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataConverter/DataConverterCollections.cs b/src/extensions/BootstrapBlazor.Socket/DataConverter/DataConverterCollections.cs new file mode 100644 index 00000000..d5ee4336 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataConverter/DataConverterCollections.cs @@ -0,0 +1,101 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; + +namespace BootstrapBlazor.DataConverters; + +/// +/// 数据转换器集合类 +/// +public sealed class DataConverterCollections +{ + readonly ConcurrentDictionary _converters = new(); + readonly ConcurrentDictionary _propertyConverters = new(); + + /// + /// 增加指定 数据类型转换器方法 + /// + /// + /// + public void AddTypeConverter(IDataConverter converter) + { + var type = typeof(TEntity); + _converters.AddOrUpdate(type, t => converter, (t, v) => converter); + } + + /// + /// 增加默认数据类型转换器方法 转换器使用 + /// + /// + public void AddTypeConverter() => AddTypeConverter(new DataConverter(this)); + + /// + /// 添加属性类型转化器方法 + /// + /// + /// + /// + public void AddPropertyConverter(Expression> propertyExpression, DataPropertyConverterAttribute attribute) + { + if (propertyExpression.Body is MemberExpression memberExpression) + { + if (attribute.Type == null) + { + attribute.Type = memberExpression.Type; + } + _propertyConverters.AddOrUpdate(memberExpression.Member, m => attribute, (m, v) => attribute); + } + } + + /// + /// 获得指定数据类型转换器方法 + /// + /// + public bool TryGetTypeConverter([NotNullWhen(true)] out IDataConverter? converter) + { + converter = null; + var ret = false; + if (_converters.TryGetValue(typeof(TEntity), out var v) && v is IDataConverter c) + { + converter = c; + ret = true; + } + return ret; + } + + /// + /// 获得指定数据类型属性转换器方法 + /// + /// + public bool TryGetPropertyConverter(Expression> propertyExpression, [NotNullWhen(true)] out DataPropertyConverterAttribute? converterAttribute) + { + converterAttribute = null; + var ret = false; + if (propertyExpression.Body is MemberExpression memberExpression && TryGetPropertyConverter(memberExpression.Member, out var v)) + { + converterAttribute = v; + ret = true; + } + return ret; + } + + /// + /// 获得指定数据类型属性转换器方法 + /// + /// + public bool TryGetPropertyConverter(MemberInfo memberInfo, [NotNullWhen(true)] out DataPropertyConverterAttribute? converterAttribute) + { + converterAttribute = null; + var ret = false; + if (_propertyConverters.TryGetValue(memberInfo, out var v)) + { + converterAttribute = v; + ret = true; + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataConverter/DataPropertyConverterAttribute.cs b/src/extensions/BootstrapBlazor.Socket/DataConverter/DataPropertyConverterAttribute.cs new file mode 100644 index 00000000..674b27e9 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataConverter/DataPropertyConverterAttribute.cs @@ -0,0 +1,45 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataConverters; + +/// +/// Represents an attribute used to mark a field as a socket data field. +/// +/// This attribute can be applied to fields to indicate that they are part of the data transmitted over a +/// socket connection. It is intended for use in scenarios where socket communication requires specific fields to be +/// identified for processing. +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public class DataPropertyConverterAttribute : Attribute +{ + /// + /// 获得/设置 数据类型 + /// + public Type? Type { get; set; } + + /// + /// 获得/设置 数据偏移量 + /// + public int Offset { get; set; } + + /// + /// 获得/设置 数据长度 + /// + public int Length { get; set; } + + /// + /// 获得/设置 数据编码名称 + /// + public string? EncodingName { get; set; } + + /// + /// 获得/设置 数据转换器类型 + /// + public Type? ConverterType { get; set; } + + /// + /// 获得/设置 数据转换器构造函数参数 + /// + public object?[]? ConverterParameters { get; set; } +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataConverter/DataTypeConverterAttribute.cs b/src/extensions/BootstrapBlazor.Socket/DataConverter/DataTypeConverterAttribute.cs new file mode 100644 index 00000000..545d8b75 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataConverter/DataTypeConverterAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataConverters; + +/// +/// +/// +[AttributeUsage(AttributeTargets.Class)] +public class DataTypeConverterAttribute : Attribute +{ + /// + /// Gets or sets the type of the . + /// + public Type? Type { get; set; } +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataConverter/IDataConverter.cs b/src/extensions/BootstrapBlazor.Socket/DataConverter/IDataConverter.cs new file mode 100644 index 00000000..23e6ad52 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataConverter/IDataConverter.cs @@ -0,0 +1,31 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataConverters; + +/// +/// Socket 数据转换器接口 +/// +public interface IDataConverter +{ + +} + +/// +/// Defines a method to convert raw socket data into a specified entity type. +/// +/// The type of entity to convert the data into. +public interface IDataConverter : IDataConverter +{ + /// + /// Attempts to convert the specified data to an instance of . + /// + /// This method does not throw an exception if the conversion fails. Instead, it returns and sets to . + /// The data to be converted, represented as a read-only memory block of bytes. + /// When this method returns, contains the converted if the conversion succeeded; + /// otherwise, . + /// if the conversion was successful; otherwise, . + bool TryConvertTo(ReadOnlyMemory data, [NotNullWhen(true)] out TEntity? entity); +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataHandler/DataPackageHandlerBase.cs b/src/extensions/BootstrapBlazor.Socket/DataHandler/DataPackageHandlerBase.cs new file mode 100644 index 00000000..91b03060 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataHandler/DataPackageHandlerBase.cs @@ -0,0 +1,68 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataHandlers; + +/// +/// Provides a base implementation for handling data packages in a communication system. +/// +/// This abstract class defines the core contract for receiving and sending data packages. Derived +/// classes should override and extend its functionality to implement specific data handling logic. The default +/// implementation simply returns the provided data. +public abstract class DataPackageHandlerBase : IDataPackageHandler +{ + private Memory _lastReceiveBuffer = Memory.Empty; + + /// + /// + /// + public Func, ValueTask>? ReceivedCallBack { get; set; } + + /// + /// + /// + /// + /// + /// + public abstract ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default); + + /// + /// Handles the processing of a sticky package by adjusting the provided buffer and length. + /// + /// This method processes the portion of the buffer beyond the specified length and updates the + /// internal state accordingly. The caller must ensure that the contains sufficient data + /// for the specified . + /// The memory buffer containing the data to process. + /// The length of the valid data within the buffer. + protected void SlicePackage(ReadOnlyMemory buffer, int length) + { + _lastReceiveBuffer = buffer[length..].ToArray().AsMemory(); + } + + /// + /// Concatenates the provided buffer with any previously stored data and returns the combined result. + /// + /// This method combines the provided buffer with any data stored in the internal buffer. After + /// concatenation, the internal buffer is cleared. The returned memory block is allocated from a shared memory pool + /// and should be used promptly to avoid holding onto pooled resources. + /// The buffer to concatenate with the previously stored data. Must not be empty. + /// A instance containing the concatenated data. If no previously stored data exists, the + /// method returns the input . + protected ReadOnlyMemory ConcatBuffer(ReadOnlyMemory buffer) + { + if (_lastReceiveBuffer.IsEmpty) + { + return buffer; + } + + // 计算缓存区长度 + Memory merged = new byte[_lastReceiveBuffer.Length + buffer.Length]; + _lastReceiveBuffer.CopyTo(merged); + buffer.CopyTo(merged[_lastReceiveBuffer.Length..]); + + // Clear the sticky buffer + _lastReceiveBuffer = Memory.Empty; + return merged; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataHandler/DelimiterDataPackageHandler.cs b/src/extensions/BootstrapBlazor.Socket/DataHandler/DelimiterDataPackageHandler.cs new file mode 100644 index 00000000..214467f3 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataHandler/DelimiterDataPackageHandler.cs @@ -0,0 +1,85 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers; +using System.Text; + +namespace BootstrapBlazor.DataHandlers; + +/// +/// Handles data packages that are delimited by a specific sequence of bytes or characters. +/// +/// This class provides functionality for processing data packages that are separated by a defined +/// delimiter. The delimiter can be specified as a string with an optional encoding or as a byte array. +public class DelimiterDataPackageHandler : DataPackageHandlerBase +{ + private readonly ReadOnlyMemory _delimiter; + + /// + /// Initializes a new instance of the class with the specified delimiter + /// and optional encoding. + /// + /// The string delimiter used to separate data packages. This value cannot be null or empty. + /// The character encoding used to convert the delimiter to bytes. If null, is used as + /// the default. + /// Thrown if is null or empty. + public DelimiterDataPackageHandler(string delimiter, Encoding? encoding = null) + { + if (string.IsNullOrEmpty(delimiter)) + { + throw new ArgumentNullException(nameof(delimiter), "Delimiter cannot be null or empty."); + } + + encoding ??= Encoding.UTF8; + _delimiter = encoding.GetBytes(delimiter); + } + + /// + /// Initializes a new instance of the class with the specified delimiters. + /// + /// An array of bytes representing the delimiters used to parse data packages. Cannot be . + /// Thrown if is . + public DelimiterDataPackageHandler(byte[] delimiter) + { + _delimiter = delimiter ?? throw new ArgumentNullException(nameof(delimiter), "Delimiter cannot be null."); + } + + /// + /// + /// + /// + /// + /// + public override async ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default) + { + data = ConcatBuffer(data); + + while (data.Length > 0) + { + var index = data.Span.IndexOfAny(_delimiter.Span); + var segment = index == -1 ? data : data[..index]; + var length = segment.Length + _delimiter.Length; + using var buffer = MemoryPool.Shared.Rent(length); + segment.CopyTo(buffer.Memory); + + if (index != -1) + { + SlicePackage(data, index + _delimiter.Length); + + _delimiter.CopyTo(buffer.Memory[index..]); + if (ReceivedCallBack != null) + { + await ReceivedCallBack(buffer.Memory[..length].ToArray()); + } + + data = data[(index + _delimiter.Length)..]; + } + else + { + SlicePackage(data, 0); + break; + } + } + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataHandler/FixLengthDataPackageHandler.cs b/src/extensions/BootstrapBlazor.Socket/DataHandler/FixLengthDataPackageHandler.cs new file mode 100644 index 00000000..baabe6af --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataHandler/FixLengthDataPackageHandler.cs @@ -0,0 +1,53 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataHandlers; + +/// +/// Handles fixed-length data packages by processing incoming data of a specified length. +/// +/// This class is designed to handle data packages with a fixed length, as specified during +/// initialization. It extends and overrides its behavior to process fixed-length +/// data. +/// The data package total data length. +public class FixLengthDataPackageHandler(int length) : DataPackageHandlerBase +{ + private readonly Memory _data = new byte[length]; + + private int _receivedLength; + + /// + /// + /// + /// + /// + /// + public override async ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default) + { + while (data.Length > 0) + { + // 拷贝数据 + var len = length - _receivedLength; + var segment = data.Length > len ? data[..len] : data; + segment.CopyTo(_data[_receivedLength..]); + + // 更新数据 + data = data[segment.Length..]; + + // 更新已接收长度 + _receivedLength += segment.Length; + + // 如果已接收长度等于总长度则触发回调 + if (_receivedLength == length) + { + // 重置已接收长度 + _receivedLength = 0; + if (ReceivedCallBack != null) + { + await ReceivedCallBack(_data); + } + } + } + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/DataHandler/IDataPackageHandler.cs b/src/extensions/BootstrapBlazor.Socket/DataHandler/IDataPackageHandler.cs new file mode 100644 index 00000000..99285265 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/DataHandler/IDataPackageHandler.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataHandlers; + +/// +/// Defines an interface for adapting data packages to and from a TCP socket connection. +/// +/// Implementations of this interface are responsible for converting raw data received from a TCP socket +/// into structured data packages and vice versa. This allows for custom serialization and deserialization logic +/// tailored to specific application protocols. +public interface IDataPackageHandler +{ + /// + /// Gets or sets the callback function to be invoked when data is received asynchronously. + /// + Func, ValueTask>? ReceivedCallBack { get; set; } + + /// + /// Asynchronously receives data and processes it. + /// + /// The method is designed for asynchronous operations and may be used in scenarios where + /// efficient handling of data streams is required. Ensure that the parameter contains valid + /// data for processing, and handle potential cancellation using the . + /// The data to be received, represented as a read-only memory block of bytes. + /// A cancellation token that can be used to cancel the operation. Defaults to if not + /// provided. + /// A containing if the data was successfully received and + /// processed; otherwise, . + ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default); +} diff --git a/src/extensions/BootstrapBlazor.Socket/Extensions/DataPropertyExtensions.cs b/src/extensions/BootstrapBlazor.Socket/Extensions/DataPropertyExtensions.cs new file mode 100644 index 00000000..76b1e143 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/Extensions/DataPropertyExtensions.cs @@ -0,0 +1,105 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataConverters; + +static class DataPropertyExtensions +{ + public static IDataPropertyConverter? GetConverter(this DataPropertyConverterAttribute attribute) + { + return attribute.GetConverterByType() ?? attribute.GetDefaultConverter(); + } + + private static IDataPropertyConverter? GetConverterByType(this DataPropertyConverterAttribute attribute) + { + IDataPropertyConverter? converter = null; + var converterType = attribute.ConverterType; + if (converterType != null) + { + var converterParameters = attribute.ConverterParameters; + var c = Activator.CreateInstance(converterType, converterParameters); + if(c is IDataPropertyConverter v) + { + converter = v; + } + } + return converter; + } + + private static IDataPropertyConverter? GetDefaultConverter(this DataPropertyConverterAttribute attribute) + { + IDataPropertyConverter? converter = null; + var type = attribute.Type; + if (type != null) + { + if (type == typeof(byte[])) + { + converter = new DataByteArrayConverter(); + } + else if (type == typeof(string)) + { + converter = new DataStringConverter(attribute.EncodingName); + } + else if (type.IsEnum) + { + converter = new DataEnumConverter(attribute.Type); + } + else if (type == typeof(bool)) + { + converter = new DataBoolConverter(); + } + else if (type == typeof(short)) + { + converter = new DataInt16BigEndianConverter(); + } + else if (type == typeof(int)) + { + converter = new DataInt32BigEndianConverter(); + } + else if (type == typeof(long)) + { + converter = new DataInt64BigEndianConverter(); + } + else if (type == typeof(float)) + { + converter = new DataSingleBigEndianConverter(); + } + else if (type == typeof(double)) + { + converter = new DataDoubleBigEndianConverter(); + } + else if (type == typeof(ushort)) + { + converter = new DataUInt16BigEndianConverter(); + } + else if (type == typeof(uint)) + { + converter = new DataUInt32BigEndianConverter(); + } + else if (type == typeof(ulong)) + { + converter = new DataUInt64BigEndianConverter(); + } + } + return converter; + } + + public static object? ConvertTo(this DataPropertyConverterAttribute attribute, ReadOnlyMemory data) + { + object? ret = null; + var start = attribute.Offset; + var length = attribute.Length; + + if (data.Length >= start + length) + { + var buffer = data.Slice(start, length); + var converter = attribute.GetConverter(); + if (converter != null) + { + ret = converter.Convert(buffer); + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataBoolConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataBoolConverter.cs new file mode 100644 index 00000000..b9aa50bc --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataBoolConverter.cs @@ -0,0 +1,25 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 bool 数据转换器 +/// +public class DataBoolConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + var ret = false; + if (data.Length == 1) + { + ret = data.Span[0] != 0x00; + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataByteArrayConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataByteArrayConverter.cs new file mode 100644 index 00000000..fb333456 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataByteArrayConverter.cs @@ -0,0 +1,20 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 byte[] 数组转换器 +/// +public class DataByteArrayConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + return data.ToArray(); + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataDoubleBigEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataDoubleBigEndianConverter.cs new file mode 100644 index 00000000..1edcb864 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataDoubleBigEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 double 数据大端转换器 +/// +public class DataDoubleBigEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + double ret = 0; + if (data.Length <= 8) + { + Span paddedSpan = stackalloc byte[8]; + data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); + if (BinaryPrimitives.TryReadDoubleBigEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataDoubleLittleEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataDoubleLittleEndianConverter.cs new file mode 100644 index 00000000..12e860f1 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataDoubleLittleEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 double 数据小端转换器 +/// +public class DataDoubleLittleEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + double ret = 0; + if (data.Length <= 8) + { + Span paddedSpan = stackalloc byte[8]; + data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); + if (BinaryPrimitives.TryReadDoubleLittleEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataEnumConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataEnumConverter.cs new file mode 100644 index 00000000..4cb53780 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataEnumConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 Enum 数据转换器 +/// +public class DataEnumConverter(Type? type) : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + object? ret = null; + if (type != null) + { + if (data.Length == 1) + { + var v = data.Span[0]; + if (Enum.TryParse(type, v.ToString(), out var enumValue)) + { + ret = enumValue; + } + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt16BigEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt16BigEndianConverter.cs new file mode 100644 index 00000000..9019c37c --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt16BigEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 short 数据大端转换器 +/// +public class DataInt16BigEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + short ret = 0; + if (data.Length <= 2) + { + Span paddedSpan = stackalloc byte[2]; + data.Span.CopyTo(paddedSpan[(2 - data.Length)..]); + if (BinaryPrimitives.TryReadInt16BigEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt16LittleEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt16LittleEndianConverter.cs new file mode 100644 index 00000000..88931d9c --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt16LittleEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 short 数据小端转换器 +/// +public class DataInt16LittleEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + short ret = 0; + if (data.Length <= 2) + { + Span paddedSpan = stackalloc byte[2]; + data.Span.CopyTo(paddedSpan[(2 - data.Length)..]); + if (BinaryPrimitives.TryReadInt16LittleEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt32BigEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt32BigEndianConverter.cs new file mode 100644 index 00000000..55263ee6 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt32BigEndianConverter.cs @@ -0,0 +1,33 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 int 数据大端转换器 +/// +public class DataInt32BigEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + var ret = 0; + if (data.Length <= 4) + { + Span paddedSpan = stackalloc byte[4]; + data.Span.CopyTo(paddedSpan[(4 - data.Length)..]); + + if (BinaryPrimitives.TryReadInt32BigEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt32LittleEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt32LittleEndianConverter.cs new file mode 100644 index 00000000..4a8090f7 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt32LittleEndianConverter.cs @@ -0,0 +1,33 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 int 数据小端转换器 +/// +public class DataInt32LittleEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + var ret = 0; + if (data.Length <= 4) + { + Span paddedSpan = stackalloc byte[4]; + data.Span.CopyTo(paddedSpan[(4 - data.Length)..]); + + if (BinaryPrimitives.TryReadInt32LittleEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt64BigEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt64BigEndianConverter.cs new file mode 100644 index 00000000..23821449 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt64BigEndianConverter.cs @@ -0,0 +1,33 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 long 数据大端转换器 +/// +public class DataInt64BigEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + long ret = 0; + if (data.Length <= 8) + { + Span paddedSpan = stackalloc byte[8]; + data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); + + if (BinaryPrimitives.TryReadInt64BigEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt64LittleEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt64LittleEndianConverter.cs new file mode 100644 index 00000000..f8a4e5b4 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt64LittleEndianConverter.cs @@ -0,0 +1,33 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 long 数据小端转换器 +/// +public class DataInt64LittleEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + long ret = 0; + if (data.Length <= 8) + { + Span paddedSpan = stackalloc byte[8]; + data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); + + if (BinaryPrimitives.TryReadInt64LittleEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataSingleBigEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataSingleBigEndianConverter.cs new file mode 100644 index 00000000..0f43bac9 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataSingleBigEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 float 数据大端转换器 +/// +public class DataSingleBigEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + float ret = 0; + if (data.Length <= 4) + { + Span paddedSpan = stackalloc byte[4]; + data.Span.CopyTo(paddedSpan[(4 - data.Length)..]); + if (BinaryPrimitives.TryReadSingleBigEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataSingleLittleEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataSingleLittleEndianConverter.cs new file mode 100644 index 00000000..a10c36d8 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataSingleLittleEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 float 数据小端转换器 +/// +public class DataSingleLittleEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + float ret = 0; + if (data.Length <= 4) + { + Span paddedSpan = stackalloc byte[4]; + data.Span.CopyTo(paddedSpan[(4 - data.Length)..]); + if (BinaryPrimitives.TryReadSingleLittleEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataStringConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataStringConverter.cs new file mode 100644 index 00000000..f10f25f2 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataStringConverter.cs @@ -0,0 +1,23 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Text; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 string 数据转换器 +/// +public class DataStringConverter(string? encodingName) : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + var encoding = string.IsNullOrEmpty(encodingName) ? Encoding.UTF8 : Encoding.GetEncoding(encodingName); + return encoding.GetString(data.Span); + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt16BigEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt16BigEndianConverter.cs new file mode 100644 index 00000000..c11ffaa8 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt16BigEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 ushort 数据大端转换器 +/// +public class DataUInt16BigEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + ushort ret = 0; + if (data.Length <= 2) + { + Span paddedSpan = stackalloc byte[2]; + data.Span.CopyTo(paddedSpan[(2 - data.Length)..]); + if (BinaryPrimitives.TryReadUInt16BigEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt16LittleEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt16LittleEndianConverter.cs new file mode 100644 index 00000000..9babd257 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt16LittleEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 ushort 数据小端转换器 +/// +public class DataUInt16LittleEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + ushort ret = 0; + if (data.Length <= 2) + { + Span paddedSpan = stackalloc byte[2]; + data.Span.CopyTo(paddedSpan[(2 - data.Length)..]); + if (BinaryPrimitives.TryReadUInt16LittleEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt32BigEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt32BigEndianConverter.cs new file mode 100644 index 00000000..c374842c --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt32BigEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 uint 数据大端转换器 +/// +public class DataUInt32BigEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + uint ret = 0; + if (data.Length <= 4) + { + Span paddedSpan = stackalloc byte[4]; + data.Span.CopyTo(paddedSpan[(4 - data.Length)..]); + if (BinaryPrimitives.TryReadUInt32BigEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt32LittleEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt32LittleEndianConverter.cs new file mode 100644 index 00000000..5c942aea --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt32LittleEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 uint 数据小端转换器 +/// +public class DataUInt32LittleEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + uint ret = 0; + if (data.Length <= 4) + { + Span paddedSpan = stackalloc byte[4]; + data.Span.CopyTo(paddedSpan[(4 - data.Length)..]); + if (BinaryPrimitives.TryReadUInt32LittleEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt64BigEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt64BigEndianConverter.cs new file mode 100644 index 00000000..12833ad7 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt64BigEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 ulong 数据大端转换器 +/// +public class DataUInt64BigEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + ulong ret = 0; + if (data.Length <= 8) + { + Span paddedSpan = stackalloc byte[8]; + data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); + if (BinaryPrimitives.TryReadUInt64BigEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt64LittleEndianConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt64LittleEndianConverter.cs new file mode 100644 index 00000000..5d0f654c --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt64LittleEndianConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers.Binary; + +namespace BootstrapBlazor.DataConverters; + +/// +/// Sokcet 数据转换为 ulong 数据小端转换器 +/// +public class DataUInt64LittleEndianConverter : IDataPropertyConverter +{ + /// + /// + /// + /// + public object? Convert(ReadOnlyMemory data) + { + ulong ret = 0; + if (data.Length <= 8) + { + Span paddedSpan = stackalloc byte[8]; + data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); + if (BinaryPrimitives.TryReadUInt64LittleEndian(paddedSpan, out var v)) + { + ret = v; + } + } + return ret; + } +} diff --git a/src/extensions/BootstrapBlazor.Socket/PropertyConverter/IDataPropertyConverter.cs b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/IDataPropertyConverter.cs new file mode 100644 index 00000000..7bcd794d --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/PropertyConverter/IDataPropertyConverter.cs @@ -0,0 +1,18 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.DataConverters; + +/// +/// Socket 数据转换器接口 +/// +public interface IDataPropertyConverter +{ + /// + /// 将数据转换为指定类型的对象 + /// + /// + /// + object? Convert(ReadOnlyMemory data); +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/BootstrapBlazor.TcpSocket.csproj b/src/extensions/BootstrapBlazor.TcpSocket/BootstrapBlazor.TcpSocket.csproj new file mode 100644 index 00000000..00d1631a --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/BootstrapBlazor.TcpSocket.csproj @@ -0,0 +1,47 @@ + + + + 9.0.0-beta01 + + + + BootstrapBlazor Socket + BootstrapBlazor extensions of TcpSocket + + + + 8.0.* + 9.0.* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClient.cs new file mode 100644 index 00000000..30e0830c --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClient.cs @@ -0,0 +1,440 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Net; +using System.Runtime.Versioning; + +namespace BootstrapBlazor.TcpSocket; + +[UnsupportedOSPlatform("browser")] +class DefaultTcpSocketClient(TcpSocketClientOptions options) : IServiceProvider, ITcpSocketClient +{ + /// + /// Gets or sets the socket client provider used for managing socket connections. + /// + private ITcpSocketClientProvider? SocketClientProvider { get; set; } + + /// + /// Gets or sets the logger instance used for logging messages and events. + /// + private ILogger? Logger { get; set; } + + /// + /// Gets or sets the service provider used to resolve dependencies. + /// + [NotNull] + public IServiceProvider? ServiceProvider { get; set; } + + /// + /// + /// + public TcpSocketClientOptions Options => options; + + /// + /// + /// + public bool IsConnected => SocketClientProvider?.IsConnected ?? false; + + /// + /// + /// + public IPEndPoint LocalEndPoint => SocketClientProvider?.LocalEndPoint ?? new IPEndPoint(IPAddress.Any, 0); + + /// + /// + /// + public Func, ValueTask>? ReceivedCallBack { get; set; } + + /// + /// + /// + public Func? OnConnecting { get; set; } + + /// + /// + /// + public Func? OnConnected { get; set; } + + private IPEndPoint? _remoteEndPoint; + private IPEndPoint? _localEndPoint; + private CancellationTokenSource? _receiveCancellationTokenSource; + private CancellationTokenSource? _autoConnectTokenSource; + + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + + /// + /// + /// + /// + /// + /// + public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + if (IsConnected) + { + return true; + } + + var connectionToken = GenerateConnectionToken(token); + try + { + await _semaphoreSlim.WaitAsync(connectionToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // 如果信号量等待被取消,则直接返回 IsConnected + // 不管是超时还是被取消,都不需要重连,肯定有其他线程在连接中 + return IsConnected; + } + + if (IsConnected) + { + _semaphoreSlim.Release(); + return true; + } + + var reconnect = true; + var ret = false; + SocketClientProvider = ServiceProvider?.GetRequiredService() + ?? throw new InvalidOperationException("SocketClientProvider is not registered in the service provider."); + + try + { + if (OnConnecting != null) + { + await OnConnecting(); + } + ret = await ConnectCoreAsync(SocketClientProvider, endPoint, connectionToken); + if (OnConnected != null) + { + await OnConnected(); + } + } + catch (OperationCanceledException ex) + { + if (token.IsCancellationRequested) + { + Log(LogLevel.Warning, ex, $"TCP Socket connect operation was canceled from {LocalEndPoint} to {endPoint}"); + reconnect = false; + } + else + { + Log(LogLevel.Warning, ex, $"TCP Socket connect operation timed out from {LocalEndPoint} to {endPoint}"); + } + } + catch (Exception ex) + { + Log(LogLevel.Error, ex, $"TCP Socket connection failed from {LocalEndPoint} to {endPoint}"); + } + + // 释放信号量 + _semaphoreSlim.Release(); + + if (reconnect) + { + _autoConnectTokenSource = new(); + + if (!ret) + { + Reconnect(); + } + } + return ret; + } + + private void Reconnect() + { + if (_autoConnectTokenSource != null && options.IsAutoReconnect && _remoteEndPoint != null) + { + Task.Run(async () => + { + try + { + await Task.Delay(options.ReconnectInterval, _autoConnectTokenSource.Token).ConfigureAwait(false); + await ConnectAsync(_remoteEndPoint, _autoConnectTokenSource.Token).ConfigureAwait(false); + } + catch { } + }, CancellationToken.None).ConfigureAwait(false); + } + } + + private async ValueTask ConnectCoreAsync(ITcpSocketClientProvider provider, IPEndPoint endPoint, CancellationToken token) + { + // 释放资源 + await CloseCoreAsync(); + + // 创建新的 TcpClient 实例 + provider.LocalEndPoint = Options.LocalEndPoint; + + _localEndPoint = Options.LocalEndPoint; + _remoteEndPoint = endPoint; + + var ret = await provider.ConnectAsync(endPoint, token); + + if (ret) + { + _localEndPoint = provider.LocalEndPoint; + + if (options.IsAutoReceive) + { + _ = Task.Run(AutoReceiveAsync, CancellationToken.None).ConfigureAwait(false); + } + } + return ret; + } + + private CancellationToken GenerateConnectionToken(CancellationToken token) + { + var connectionToken = token; + if (Options.ConnectTimeout > 0) + { + // 设置连接超时时间 + var connectTokenSource = new CancellationTokenSource(options.ConnectTimeout); + connectionToken = CancellationTokenSource.CreateLinkedTokenSource(token, connectTokenSource.Token).Token; + } + return connectionToken; + } + + /// + /// + /// + /// + /// + /// + public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + if (SocketClientProvider is not { IsConnected: true }) + { + throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); + } + + var ret = false; + var reconnect = true; + try + { + var sendToken = token; + if (options.SendTimeout > 0) + { + // 设置发送超时时间 + var sendTokenSource = new CancellationTokenSource(options.SendTimeout); + sendToken = CancellationTokenSource.CreateLinkedTokenSource(token, sendTokenSource.Token).Token; + } + ret = await SocketClientProvider.SendAsync(data, sendToken); + } + catch (OperationCanceledException ex) + { + if (token.IsCancellationRequested) + { + reconnect = false; + Log(LogLevel.Warning, ex, $"TCP Socket send operation was canceled from {_localEndPoint} to {_remoteEndPoint}"); + } + else + { + Log(LogLevel.Warning, ex, $"TCP Socket send operation timed out from {_localEndPoint} to {_remoteEndPoint}"); + } + } + catch (Exception ex) + { + Log(LogLevel.Error, ex, $"TCP Socket send failed from {_localEndPoint} to {_remoteEndPoint}"); + } + + Log(LogLevel.Information, null, $"Sending data from {_localEndPoint} to {_remoteEndPoint}, Data Length: {data.Length} Data Content: {BitConverter.ToString(data.ToArray())} Result: {ret}"); + + if (!ret && reconnect) + { + // 如果发送失败并且需要重连则尝试重连 + Reconnect(); + } + return ret; + } + + /// + /// + /// + /// + /// + public async ValueTask> ReceiveAsync(CancellationToken token = default) + { + if (SocketClientProvider is not { IsConnected: true }) + { + throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); + } + + if (options.IsAutoReceive) + { + throw new InvalidOperationException("Cannot call ReceiveAsync when IsAutoReceive is true. Use the auto-receive mechanism instead."); + } + + using var block = MemoryPool.Shared.Rent(options.ReceiveBufferSize); + var buffer = block.Memory; + var len = await ReceiveCoreAsync(SocketClientProvider, buffer, token); + if (len == 0) + { + Reconnect(); + } + return buffer[..len]; + } + + private async ValueTask AutoReceiveAsync() + { + // 自动接收方法 + _receiveCancellationTokenSource ??= new(); + while (_receiveCancellationTokenSource is { IsCancellationRequested: false }) + { + if (SocketClientProvider is not { IsConnected: true }) + { + throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); + } + + using var block = MemoryPool.Shared.Rent(options.ReceiveBufferSize); + var buffer = block.Memory; + var len = await ReceiveCoreAsync(SocketClientProvider, buffer, _receiveCancellationTokenSource.Token); + if (len == 0) + { + // 远端关闭或者 DisposeAsync 方法被调用时退出 + break; + } + } + + Reconnect(); + } + + private async ValueTask ReceiveCoreAsync(ITcpSocketClientProvider client, Memory buffer, CancellationToken token) + { + var reconnect = true; + var len = 0; + try + { + var receiveToken = token; + if (options.ReceiveTimeout > 0) + { + // 设置接收超时时间 + var receiveTokenSource = new CancellationTokenSource(options.ReceiveTimeout); + receiveToken = CancellationTokenSource.CreateLinkedTokenSource(receiveToken, receiveTokenSource.Token).Token; + } + + len = await client.ReceiveAsync(buffer, receiveToken); + if (len == 0) + { + // 远端主机关闭链路 + Log(LogLevel.Information, null, $"TCP Socket {_localEndPoint} received 0 data closed by {_remoteEndPoint}"); + buffer = Memory.Empty; + } + else + { + buffer = buffer[..len]; + } + + if (ReceivedCallBack != null) + { + // 如果订阅回调则触发回调 + await ReceivedCallBack(buffer); + } + } + catch (OperationCanceledException ex) + { + if (token.IsCancellationRequested) + { + Log(LogLevel.Warning, ex, $"TCP Socket receive operation canceled from {_localEndPoint} to {_remoteEndPoint}"); + reconnect = false; + } + else + { + Log(LogLevel.Warning, ex, $"TCP Socket receive operation timed out from {_localEndPoint} to {_remoteEndPoint}"); + } + } + catch (Exception ex) + { + Log(LogLevel.Error, ex, $"TCP Socket receive failed from {_localEndPoint} to {_remoteEndPoint}"); + } + + Log(LogLevel.Information, null, $"Receiving data from {_localEndPoint} to {_remoteEndPoint}, Data Length: {len} Data Content: {BitConverter.ToString(buffer.ToArray())}"); + + if (len == 0 && reconnect) + { + // 如果接收数据长度为 0 并且需要重连则尝试重连 + Reconnect(); + } + return len; + } + + /// + /// Logs a message with the specified log level, exception, and additional context. + /// + private void Log(LogLevel logLevel, Exception? ex, string? message) + { + if (options.EnableLog) + { + Logger ??= ServiceProvider?.GetRequiredService>(); + Logger?.Log(logLevel, ex, "{Message}", message); + } + } + + /// + /// + /// + public async ValueTask CloseAsync() + { + // 取消重连任务 + if (_autoConnectTokenSource != null) + { + _autoConnectTokenSource.Cancel(); + _autoConnectTokenSource.Dispose(); + _autoConnectTokenSource = null; + } + + await CloseCoreAsync(); + } + + private async ValueTask CloseCoreAsync() + { + // 取消接收数据的任务 + if (_receiveCancellationTokenSource != null) + { + _receiveCancellationTokenSource.Cancel(); + _receiveCancellationTokenSource.Dispose(); + _receiveCancellationTokenSource = null; + } + + if (SocketClientProvider != null) + { + await SocketClientProvider.CloseAsync(); + } + } + + /// + /// + /// + /// + /// + public object? GetService(Type serviceType) => ServiceProvider.GetService(serviceType); + + /// + /// Releases the resources used by the current instance of the class. + /// + /// This method is called to free both managed and unmanaged resources. If the parameter is , the method releases managed resources in addition to + /// unmanaged resources. Override this method in a derived class to provide custom cleanup logic. + /// to release both managed and unmanaged resources; to release only + /// unmanaged resources. + private async ValueTask DisposeAsync(bool disposing) + { + if (disposing) + { + await CloseAsync(); + } + } + + /// + /// + /// + public async ValueTask DisposeAsync() + { + await DisposeAsync(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClientProvider.cs b/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClientProvider.cs new file mode 100644 index 00000000..5d31cf4d --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClientProvider.cs @@ -0,0 +1,92 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Net; +using System.Net.Sockets; +using System.Runtime.Versioning; + +namespace BootstrapBlazor.TcpSocket; + +/// +/// TcpSocket 客户端默认实现 +/// +[UnsupportedOSPlatform("browser")] +class DefaultTcpSocketClientProvider : ITcpSocketClientProvider +{ + private TcpClient? _client; + + /// + /// + /// + public bool IsConnected => _client?.Connected ?? false; + + /// + /// + /// + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); + + /// + /// + /// + public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + _client = new TcpClient(LocalEndPoint); + await _client.ConnectAsync(endPoint, token).ConfigureAwait(false); + if (_client.Connected) + { + if (_client.Client.LocalEndPoint is IPEndPoint localEndPoint) + { + LocalEndPoint = localEndPoint; + } + } + return _client.Connected; + } + + /// + /// + /// + public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + var ret = false; + if (_client != null) + { + var stream = _client.GetStream(); + await stream.WriteAsync(data, token).ConfigureAwait(false); + ret = true; + } + return ret; + } + + /// + /// + /// + public async ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default) + { + var len = 0; + if (_client is { Connected: true }) + { + var stream = _client.GetStream(); + len = await stream.ReadAsync(buffer, token).ConfigureAwait(false); + + if (len == 0) + { + _client.Close(); + } + } + return len; + } + + /// + /// + /// + public ValueTask CloseAsync() + { + if (_client != null) + { + _client.Close(); + _client = null; + } + return ValueTask.CompletedTask; + } +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketFactory.cs b/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketFactory.cs new file mode 100644 index 00000000..627a579b --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketFactory.cs @@ -0,0 +1,60 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Collections.Concurrent; +using System.Runtime.Versioning; + +namespace BootstrapBlazor.TcpSocket; + +[UnsupportedOSPlatform("browser")] +sealed class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory +{ + private readonly ConcurrentDictionary _pool = new(); + + public ITcpSocketClient GetOrCreate(string name, Action valueFactory) + { + return _pool.GetOrAdd(name, key => + { + var options = new TcpSocketClientOptions(); + valueFactory(options); + var client = new DefaultTcpSocketClient(options) + { + ServiceProvider = provider, + }; + return client; + }); + } + + public ITcpSocketClient? Remove(string name) + { + ITcpSocketClient? client = null; + if (_pool.TryRemove(name, out var c)) + { + client = c; + } + return client; + } + + private async ValueTask DisposeAsync(bool disposing) + { + if (disposing) + { + // 释放托管资源 + foreach (var socket in _pool.Values) + { + await socket.DisposeAsync(); + } + _pool.Clear(); + } + } + + /// + /// + /// + public async ValueTask DisposeAsync() + { + await DisposeAsync(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs b/src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs new file mode 100644 index 00000000..f02ecb34 --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System.Reflection; +using System.Runtime.Versioning; +using System.Text; + +namespace BootstrapBlazor.TcpSocket; + +/// +/// 扩展方法类 +/// +[UnsupportedOSPlatform("browser")] +public static class ITcpSocketClientExtensions +{ + /// + /// Sends the specified string content to the connected TCP socket client asynchronously. + /// + /// This method converts the provided string content into a byte array using the specified + /// encoding (or UTF-8 by default) and sends it to the connected TCP socket client. Ensure the client is connected + /// before calling this method. + /// The TCP socket client to which the content will be sent. Cannot be . + /// The string content to send. Cannot be or empty. + /// The character encoding to use for converting the string content to bytes. If , UTF-8 + /// encoding is used by default. + /// A to observe while waiting for the operation to complete. + /// A that represents the asynchronous operation. The result is if the content was sent successfully; otherwise, . + public static ValueTask SendAsync(this ITcpSocketClient client, string content, Encoding? encoding = null, CancellationToken token = default) + { + var buffer = encoding?.GetBytes(content) ?? Encoding.UTF8.GetBytes(content); + return client.SendAsync(buffer, token); + } + + /// + /// Establishes an asynchronous connection to the specified host and port. + /// + /// The TCP socket client to which the content will be sent. Cannot be . + /// The hostname or IP address of the server to connect to. Cannot be null or empty. + /// The port number on the server to connect to. Must be a valid port number between 0 and 65535. + /// An optional to cancel the connection attempt. Defaults to if not provided. + /// A task that represents the asynchronous operation. The task result is if the connection + /// is successfully established; otherwise, . + public static ValueTask ConnectAsync(this ITcpSocketClient client, string ipString, int port, CancellationToken token = default) + { + var endPoint = TcpSocketUtility.ConvertToIpEndPoint(ipString, port); + return client.ConnectAsync(endPoint, token); + } + + /// + /// Configures the specified to use the provided + /// for processing received data and sets a callback to handle processed data. + /// + /// This method sets up a two-way data processing pipeline: + /// The is configured to pass received data to the + /// for processing. The is configured to invoke + /// the provided with the processed data. Use this method + /// to integrate a custom data processing adapter with a TCP socket client. + /// The instance to configure. + /// The used to process incoming data. + /// A callback function invoked with the processed data. The function receives a + /// containing the processed data and returns a . + public static void SetDataPackageAdapter(this ITcpSocketClient client, IDataPackageAdapter adapter, Func, ValueTask> callback) + { + // 设置 ITcpSocketClient 的回调函数 + client.ReceivedCallBack = async buffer => + { + // 将接收到的数据传递给 DataPackageAdapter 进行数据处理合规数据触发 ReceivedCallBack 回调 + await adapter.HandlerAsync(buffer); + }; + + // 设置 DataPackageAdapter 的回调函数 + adapter.ReceivedCallBack = buffer => callback(buffer); + } + + /// + /// Configures the specified to use a data package adapter and a callback function + /// for processing received data. + /// + /// This method sets up the to process incoming data using the + /// specified and . The is called with the converted entity whenever data is received. + /// The type of the entity that the data will be converted to. + /// The TCP socket client to configure. + /// The data package adapter responsible for handling incoming data. + /// The converter used to transform the received data into the specified entity type. + /// The callback function to be invoked with the converted entity. + public static void SetDataPackageAdapter(this ITcpSocketClient client, IDataPackageAdapter adapter, IDataConverter socketDataConverter, Func callback) + { + // 设置 ITcpSocketClient 的回调函数 + client.ReceivedCallBack = async buffer => + { + // 将接收到的数据传递给 DataPackageAdapter 进行数据处理合规数据触发 ReceivedCallBack 回调 + await adapter.HandlerAsync(buffer); + }; + + // 设置 DataPackageAdapter 的回调函数 + adapter.ReceivedCallBack = async buffer => + { + TEntity? ret = default; + if (socketDataConverter.TryConvertTo(buffer, out var t)) + { + ret = t; + } + await callback(ret); + }; + } + + /// + /// Configures the specified to use a custom data package adapter and callback + /// function. + /// + /// This method sets up the to use the specified for handling incoming data. If the type is decorated with a , the associated converter is used to transform the data before invoking + /// the . The callback is called with the converted entity or if + /// conversion fails. + /// The type of entity that the data package adapter will handle. + /// The TCP socket client to configure. + /// The data package adapter responsible for processing incoming data. + /// The callback function to invoke with the processed entity of type . + public static void SetDataPackageAdapter(this ITcpSocketClient client, IDataPackageAdapter adapter, Func callback) + { + // 设置 ITcpSocketClient 的回调函数 + client.ReceivedCallBack = async buffer => + { + // 将接收到的数据传递给 DataPackageAdapter 进行数据处理合规数据触发 ReceivedCallBack 回调 + await adapter.HandlerAsync(buffer); + }; + + IDataConverter? converter = null; + + var type = typeof(TEntity); + var converterType = type.GetCustomAttribute(); + if (converterType is { Type: not null }) + { + // 如果类型上有 SocketDataTypeConverterAttribute 特性则使用特性中指定的转换器 + if (Activator.CreateInstance(converterType.Type) is IDataConverter socketDataConverter) + { + converter = socketDataConverter; + } + } + else + { + // 如果没有特性则从 ITcpSocketClient 中的服务容器获取转换器 + converter = client.GetSocketDataConverter(); + } + + if (converter == null) + { + // 设置正常回调 + adapter.ReceivedCallBack = async buffer => await callback(default); + } + else + { + // 设置转化器 + adapter.SetDataAdapterCallback(converter, callback); + } + } + + private static void SetDataAdapterCallback(this IDataPackageAdapter adapter, IDataConverter converter, Func callback) + { + adapter.ReceivedCallBack = async buffer => + { + TEntity? ret = default; + if (converter.TryConvertTo(buffer, out var t)) + { + ret = t; + } + await callback(ret); + }; + } + + private static IDataConverter? GetSocketDataConverter(this ITcpSocketClient client) + { + IDataConverter? converter = null; + if (client is IServiceProvider provider) + { + var converters = provider.GetRequiredService>().Value; + if (converters.TryGetTypeConverter(out var v)) + { + converter = v; + } + } + return converter; + } +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/Extensions/TcpSocketExtensions.cs b/src/extensions/BootstrapBlazor.TcpSocket/Extensions/TcpSocketExtensions.cs new file mode 100644 index 00000000..3371ccb4 --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/Extensions/TcpSocketExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Runtime.Versioning; + +namespace BootstrapBlazor.TcpSocket; + +/// +/// TcpSocket 扩展方法 +/// +[UnsupportedOSPlatform("browser")] +public static class TcpSocketExtensions +{ + /// + /// 增加 ITcpSocketFactory 服务 + /// + /// + /// + public static IServiceCollection AddBootstrapBlazorTcpSocketFactory(this IServiceCollection services) + { + // 添加 ITcpSocketFactory 服务 + services.AddSingleton(); + + // 增加 ISocketClientProvider 服务 + services.TryAddTransient(); + + return services; + } + + /// + /// 配置第三方数据模型与 数据转换器集合配置扩展方法 + /// + /// + /// + /// + public static IServiceCollection ConfigureDataConverters(this IServiceCollection services, Action configureOptions) + { + services.Configure(configureOptions); + return services; + } +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/Extensions/TcpSocketUtility.cs b/src/extensions/BootstrapBlazor.TcpSocket/Extensions/TcpSocketUtility.cs new file mode 100644 index 00000000..1118828a --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/Extensions/TcpSocketUtility.cs @@ -0,0 +1,74 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Net; +using System.Net.Sockets; +using System.Runtime.Versioning; + +namespace BootstrapBlazor.TcpSocket; + +/// +/// SocketUtility 帮助类 +/// +public static class TcpSocketUtility +{ + /// + /// Converts a string representation of an IP address or hostname into an object. + /// + /// This method handles common special cases for IP address strings, such as "localhost" and + /// "any". For other inputs, it attempts to parse the string as an IP address using . If parsing fails, the method resolves the input as a + /// hostname. + /// A string containing the IP address or hostname to convert. Special values include: + /// "localhost" returns the loopback address (). "any" returns the wildcard address + /// (). For other values, the method attempts to parse the + /// string as an IP address or resolve it as a hostname. + /// An object representing the parsed or resolved IP address. If the input cannot be parsed + /// or resolved, the method returns a default IP address. + [UnsupportedOSPlatform("browser")] + public static IPAddress ConvertToIPAddress(string ipString) + { + if (string.IsNullOrEmpty(ipString)) + { + throw new ArgumentNullException(nameof(ipString), "IP address cannot be null or empty."); + } + + if (ipString.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + return IPAddress.Loopback; + } + if (ipString.Equals("any", StringComparison.OrdinalIgnoreCase)) + { + return IPAddress.Any; + } + + return IPAddress.TryParse(ipString, out var ip) ? ip : IPAddressByHostName; + } + + [ExcludeFromCodeCoverage] + + [UnsupportedOSPlatform("browser")] + private static IPAddress IPAddressByHostName => Dns.GetHostAddresses(Dns.GetHostName(), AddressFamily.InterNetwork).FirstOrDefault() ?? IPAddress.Any; + + /// + /// Converts a string representation of an IP address and a port number into an instance. + /// + /// This method is not supported on browser platforms. + /// The string representation of the IP address. Must be a valid IPv4 or IPv6 address. + /// The port number associated with the endpoint. Must be between 0 and 65535. + /// An representing the specified IP address and port. + /// Thrown if is less than 0 or greater than 65535. + [UnsupportedOSPlatform("browser")] + public static IPEndPoint ConvertToIpEndPoint(string ipString, int port) + { + if (port < 0 || port > 65535) + { + throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 0 and 65535."); + } + + var address = ConvertToIPAddress(ipString); + return new IPEndPoint(address, port); + } +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClient.cs b/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClient.cs new file mode 100644 index 00000000..dcf58028 --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClient.cs @@ -0,0 +1,92 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Net; + +namespace BootstrapBlazor.TcpSocket; + +/// +/// Represents a TCP socket for network communication. +/// +public interface ITcpSocketClient : IAsyncDisposable +{ + /// + /// Gets a value indicating whether the system is currently connected. Default is false. + /// + bool IsConnected { get; } + + /// + /// Gets or sets the configuration options for the socket client. + /// + TcpSocketClientOptions Options { get; } + + /// + /// Gets the local network endpoint that the socket is bound to. + /// + /// This property provides information about the local endpoint of the socket, which is typically + /// used to identify the local address and port being used for communication. If the socket is not bound to a + /// specific local endpoint, this property may return . + IPEndPoint LocalEndPoint { get; } + + /// + /// Gets or sets the callback function to handle received data. + /// + /// The callback function should be designed to handle the received data efficiently and + /// asynchronously. Ensure that the implementation does not block or perform long-running operations, as this may + /// impact performance. + Func, ValueTask>? ReceivedCallBack { get; set; } + + /// + /// Gets or sets the callback function that is invoked when a connection attempt is initiated. + /// + Func? OnConnecting { get; set; } + + /// + /// Gets or sets the delegate to be invoked when a connection is successfully established. + /// + Func? OnConnected { get; set; } + + /// + /// Establishes an asynchronous connection to the specified endpoint. + /// + /// This method attempts to establish a connection to the specified endpoint. If the connection + /// cannot be established, the method returns rather than throwing an exception. + /// The representing the remote endpoint to connect to. Cannot be null. + /// A that can be used to cancel the connection attempt. Defaults to if not provided. + /// A task that represents the asynchronous operation. The task result is if the connection + /// is successfully established; otherwise, . + ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default); + + /// + /// Sends the specified data asynchronously to the target endpoint. + /// + /// This method performs a non-blocking operation to send data. If the operation is canceled via + /// the , the task will complete with a canceled state. Ensure the connection is properly + /// initialized before calling this method. + /// The byte array containing the data to be sent. Cannot be null or empty. + /// An optional to observe while waiting for the operation to complete. + /// A task that represents the asynchronous operation. The task result is if the data was + /// sent successfully; otherwise, . + ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default); + + /// + /// Asynchronously receives a block of data from the underlying source. + /// + /// This method is non-blocking and completes when data is available or the operation is + /// canceled. If the operation is canceled, the returned task will be in a faulted state with a . + /// A cancellation token that can be used to cancel the operation. The default value is . + /// A containing a of bytes representing the received data. + /// The returned memory may be empty if no data is available. + ValueTask> ReceiveAsync(CancellationToken token = default); + + /// + /// Closes the current connection or resource, releasing any associated resources. + /// + /// Once the connection or resource is closed, it cannot be reopened. Ensure that all necessary + /// operations are completed before calling this method. This method is typically used to clean up resources when + /// they are no longer needed. + ValueTask CloseAsync(); +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClientProvider.cs b/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClientProvider.cs new file mode 100644 index 00000000..14647fa6 --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClientProvider.cs @@ -0,0 +1,72 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Net; + +namespace BootstrapBlazor.TcpSocket; + +/// +/// Defines the contract for a socket client that provides asynchronous methods for connecting, sending, receiving, and +/// closing network connections. +/// +/// This interface is designed to facilitate network communication using sockets. It provides methods for +/// establishing connections, transmitting data, and receiving data asynchronously. Implementations of this interface +/// should ensure proper resource management, including closing connections and releasing resources when no longer +/// needed. +public interface ITcpSocketClientProvider +{ + /// + /// Gets a value indicating whether the connection is currently active. + /// + bool IsConnected { get; } + + /// + /// Gets the local network endpoint that the socket is bound to. + /// + IPEndPoint LocalEndPoint { get; set; } + + /// + /// Establishes an asynchronous connection to the specified endpoint. + /// + /// This method attempts to establish a connection to the specified endpoint. If the connection + /// fails, the method returns rather than throwing an exception. Ensure the endpoint is + /// valid and reachable before calling this method. + /// The representing the remote endpoint to connect to. + /// An optional to observe while waiting for the connection to complete. + /// A that represents the asynchronous operation. The result is if the connection was successfully established; otherwise, . + ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default); + + /// + /// Sends the specified data asynchronously to the connected endpoint. + /// + /// This method performs a non-blocking operation to send data. If the operation is canceled via + /// the , the returned task will not complete successfully. Ensure the connected endpoint + /// is ready to receive data before calling this method. + /// The data to send, represented as a read-only memory block of bytes. + /// An optional cancellation token that can be used to cancel the operation. + /// A representing the asynchronous operation. The result is if the data was sent successfully; otherwise, . + ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default); + + /// + /// Asynchronously receives data from a source and writes it into the specified buffer. + /// + /// This method does not guarantee that the buffer will be completely filled. The caller should + /// check the return value to determine the number of bytes received. + /// The memory buffer where the received data will be stored. Must be large enough to hold the incoming data. + /// A cancellation token that can be used to cancel the operation. Defaults to if not + /// provided. + /// A representing the asynchronous operation. The result is the number of bytes + /// successfully received and written into the buffer. Returns 0 if the end of the data stream is reached. + ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default); + + /// + /// Closes the current connection or resource, releasing any associated resources. + /// + /// Once the connection or resource is closed, it cannot be reopened. Ensure that all necessary + /// operations are completed before calling this method. This method is typically used to clean up resources when + /// they are no longer needed. + ValueTask CloseAsync(); +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketFactory.cs b/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketFactory.cs new file mode 100644 index 00000000..b791809c --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketFactory.cs @@ -0,0 +1,29 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.TcpSocket; + +/// +/// ITcpSocketFactory Interface +/// +public interface ITcpSocketFactory : IAsyncDisposable +{ + /// + /// Retrieves an existing TCP socket client by name or creates a new one using the specified configuration. + /// + /// The unique name of the TCP socket client to retrieve or create. Cannot be null or empty. + /// A delegate used to configure the for the new TCP socket client if it does not + /// already exist. This delegate is invoked only when a new client is created. + /// An instance of corresponding to the specified name. If the client already exists, + /// the existing instance is returned; otherwise, a new instance is created and returned. + ITcpSocketClient GetOrCreate(string name, Action valueFactory); + + /// + /// Removes the TCP socket client associated with the specified name. + /// + /// The name of the TCP socket client to remove. Cannot be or empty. + /// The removed instance if a client with the specified name exists; otherwise, . + ITcpSocketClient? Remove(string name); +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/TcpSocketClientOptions.cs b/src/extensions/BootstrapBlazor.TcpSocket/TcpSocketClientOptions.cs new file mode 100644 index 00000000..40a43441 --- /dev/null +++ b/src/extensions/BootstrapBlazor.TcpSocket/TcpSocketClientOptions.cs @@ -0,0 +1,67 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Net; + +namespace BootstrapBlazor.TcpSocket; + +/// +/// Represents configuration options for a socket client, including buffer sizes, timeouts, and endpoints. +/// +/// Use this class to configure various settings for a socket client, such as connection timeouts, +/// buffer sizes, and local or remote endpoints. These options allow fine-tuning of socket behavior to suit specific +/// networking scenarios. +public class TcpSocketClientOptions +{ + /// + /// Gets or sets the size, in bytes, of the receive buffer used by the connection. + /// + public int ReceiveBufferSize { get; set; } = 1024 * 64; + + /// + /// Gets or sets a value indicating whether automatic receiving data is enabled. Default is true. + /// + public bool IsAutoReceive { get; set; } = true; + + /// + /// Gets or sets the timeout duration, in milliseconds, for establishing a connection. + /// + public int ConnectTimeout { get; set; } + + /// + /// Gets or sets the duration, in milliseconds, to wait for a send operation to complete before timing out. + /// + /// If the send operation does not complete within the specified timeout period, an exception may + /// be thrown. + public int SendTimeout { get; set; } + + /// + /// Gets or sets the amount of time, in milliseconds, that the receiver will wait for a response before timing out. + /// + /// Use this property to configure the maximum wait time for receiving a response. Setting an + /// appropriate timeout can help prevent indefinite blocking in scenarios where responses may be delayed or + /// unavailable. + public int ReceiveTimeout { get; set; } + + /// + /// Gets or sets the local endpoint for the socket client. Default value is + /// + /// This property specifies the local network endpoint that the socket client will bind to when establishing a connection. + public IPEndPoint LocalEndPoint { get; set; } = new(IPAddress.Any, 0); + + /// + /// Gets or sets a value indicating whether logging is enabled. Default value is false. + /// + public bool EnableLog { get; set; } + + /// + /// Gets or sets a value indicating whether the system should automatically attempt to reconnect after a connection is lost. Default value is false. + /// + public bool IsAutoReconnect { get; set; } + + /// + /// Gets or sets the interval, in milliseconds, between reconnection attempts. Default value is 5000. + /// + public int ReconnectInterval { get; set; } = 5000; +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 5cfa559e..3cc85025 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -8,7 +8,7 @@ - net8.0 + net9.0 diff --git a/test/UniTestSvgIcon/AntDesign/filled.svg b/test/UnitTestSvgIcon/AntDesign/filled.svg similarity index 100% rename from test/UniTestSvgIcon/AntDesign/filled.svg rename to test/UnitTestSvgIcon/AntDesign/filled.svg diff --git a/test/UniTestSvgIcon/AntDesign/filled.zip b/test/UnitTestSvgIcon/AntDesign/filled.zip similarity index 100% rename from test/UniTestSvgIcon/AntDesign/filled.zip rename to test/UnitTestSvgIcon/AntDesign/filled.zip diff --git a/test/UniTestSvgIcon/AntDesign/outlined.svg b/test/UnitTestSvgIcon/AntDesign/outlined.svg similarity index 100% rename from test/UniTestSvgIcon/AntDesign/outlined.svg rename to test/UnitTestSvgIcon/AntDesign/outlined.svg diff --git a/test/UniTestSvgIcon/AntDesign/outlined.zip b/test/UnitTestSvgIcon/AntDesign/outlined.zip similarity index 100% rename from test/UniTestSvgIcon/AntDesign/outlined.zip rename to test/UnitTestSvgIcon/AntDesign/outlined.zip diff --git a/test/UniTestSvgIcon/AntDesign/twotone.svg b/test/UnitTestSvgIcon/AntDesign/twotone.svg similarity index 100% rename from test/UniTestSvgIcon/AntDesign/twotone.svg rename to test/UnitTestSvgIcon/AntDesign/twotone.svg diff --git a/test/UniTestSvgIcon/AntDesign/twotone.zip b/test/UnitTestSvgIcon/AntDesign/twotone.zip similarity index 100% rename from test/UniTestSvgIcon/AntDesign/twotone.zip rename to test/UnitTestSvgIcon/AntDesign/twotone.zip diff --git a/test/UniTestSvgIcon/Element/element.zip b/test/UnitTestSvgIcon/Element/element.zip similarity index 100% rename from test/UniTestSvgIcon/Element/element.zip rename to test/UnitTestSvgIcon/Element/element.zip diff --git a/test/UniTestSvgIcon/IconPark/download.zip b/test/UnitTestSvgIcon/IconPark/download.zip similarity index 100% rename from test/UniTestSvgIcon/IconPark/download.zip rename to test/UnitTestSvgIcon/IconPark/download.zip diff --git a/test/UniTestSvgIcon/OctIcon/octicons.zip b/test/UnitTestSvgIcon/OctIcon/octicons.zip similarity index 100% rename from test/UniTestSvgIcon/OctIcon/octicons.zip rename to test/UnitTestSvgIcon/OctIcon/octicons.zip diff --git a/test/UniTestSvgIcon/UnitTest.cs b/test/UnitTestSvgIcon/UnitTest.cs similarity index 100% rename from test/UniTestSvgIcon/UnitTest.cs rename to test/UnitTestSvgIcon/UnitTest.cs diff --git a/test/UniTestSvgIcon/UniTestSvgIcon.csproj b/test/UnitTestSvgIcon/UnitTestSvgIcon.csproj similarity index 100% rename from test/UniTestSvgIcon/UniTestSvgIcon.csproj rename to test/UnitTestSvgIcon/UnitTestSvgIcon.csproj diff --git a/test/UniTestSvgIcon/Univer/univer.zip b/test/UnitTestSvgIcon/Univer/univer.zip similarity index 100% rename from test/UniTestSvgIcon/Univer/univer.zip rename to test/UnitTestSvgIcon/Univer/univer.zip diff --git a/test/UnitTestTcpSocket/DefaultSocketClientProviderTest.cs b/test/UnitTestTcpSocket/DefaultSocketClientProviderTest.cs new file mode 100644 index 00000000..61658a22 --- /dev/null +++ b/test/UnitTestTcpSocket/DefaultSocketClientProviderTest.cs @@ -0,0 +1,108 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Net; +using System.Net.Sockets; + +namespace UnitTestTcpSocket; + +public class DefaultSocketClientProviderTest +{ + [Fact] + public async Task DefaultSocketClient_Ok() + { + var sc = new ServiceCollection(); + sc.AddBootstrapBlazorTcpSocketFactory(); + var provider = sc.BuildServiceProvider(); + var clientProvider = provider.GetRequiredService(); + + // 未建立连接时 IsConnected 应为 false + Assert.False(clientProvider.IsConnected); + + // 未建立连接直接调用 ReceiveAsync 方法 + var buffer = new byte[1024]; + var len = await clientProvider.ReceiveAsync(buffer); + Assert.Equal(0, len); + } + + [Fact] + public async Task ReceiveAsync_Ok() + { + var port = 8100; + // 测试接收数据时服务器断开未连接的情况 + StartTcpServer(port); + + var sc = new ServiceCollection(); + sc.AddBootstrapBlazorTcpSocketFactory(); + var provider = sc.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + var client = factory.GetOrCreate("provider", op => + { + op.LocalEndPoint = TcpSocketUtility.ConvertToIpEndPoint("localhost", 0); + op.IsAutoReceive = false; + op.EnableLog = false; + }); + + await client.ConnectAsync("127.0.0.1", port); + Assert.True(client.IsConnected); + + var buffer = await client.ReceiveAsync(); + Assert.Equal(2, buffer.Length); + + await Task.Delay(50); + buffer = await client.ReceiveAsync(); + Assert.False(client.IsConnected); + } + + [Fact] + public void SocketClientOptions_Ok() + { + var options = new TcpSocketClientOptions + { + ReceiveBufferSize = 1024 * 64, + IsAutoReceive = true, + ConnectTimeout = 1000, + SendTimeout = 500, + ReceiveTimeout = 500, + LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0) + }; + Assert.Equal(1024 * 64, options.ReceiveBufferSize); + Assert.True(options.IsAutoReceive); + Assert.Equal(1000, options.ConnectTimeout); + Assert.Equal(500, options.SendTimeout); + Assert.Equal(500, options.ReceiveTimeout); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 0), options.LocalEndPoint); + } + + private static TcpListener StartTcpServer(int port) + { + var server = new TcpListener(IPAddress.Loopback, port); + server.Start(); + Task.Run(() => AcceptClientsAsync(server)); + return server; + } + + private static async Task AcceptClientsAsync(TcpListener server) + { + while (true) + { + var client = await server.AcceptTcpClientAsync(); + _ = Task.Run(async () => + { + using var stream = client.GetStream(); + while (true) + { + var buffer = new byte[1024]; + + // 模拟拆包发送第二段数据 + await stream.WriteAsync(new byte[] { 0x3, 0x4 }, CancellationToken.None); + + // 等待 20ms + await Task.Delay(20); + client.Close(); + } + }); + } + } +} diff --git a/test/UnitTestTcpSocket/Foo.cs b/test/UnitTestTcpSocket/Foo.cs new file mode 100644 index 00000000..e244b923 --- /dev/null +++ b/test/UnitTestTcpSocket/Foo.cs @@ -0,0 +1,94 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.ComponentModel.DataAnnotations; + +namespace UnitTestTcpSocket; + +/// +/// Demo示例数据 +/// Demo sample data +/// +public class Foo +{ + // 列头信息支持 Display DisplayName 两种标签 + + /// + /// 主键 + /// + [Key] + [Display(Name = "主键")] + public int Id { get; set; } + + /// + /// 姓名 + /// + [Required(ErrorMessage = "{0}不能为空")] + [Display(Name = "姓名")] + public string? Name { get; set; } + + /// + /// 日期 + /// + [Display(Name = "日期")] + public DateTime? DateTime { get; set; } + + /// + /// 地址 + /// + [Display(Name = "地址")] + [Required(ErrorMessage = "{0}不能为空")] + public string? Address { get; set; } + + /// + /// 数量 + /// + [Display(Name = "数量")] + [Required] + public int Count { get; set; } + + /// + /// 是/否 + /// + [Display(Name = "是/否")] + public bool Complete { get; set; } + + /// + /// 学历 + /// + [Required(ErrorMessage = "请选择学历")] + [Display(Name = "学历")] + public EnumEducation? Education { get; set; } + + /// + /// 爱好 + /// + [Required(ErrorMessage = "请选择一种{0}")] + [Display(Name = "爱好")] + public IEnumerable Hobby { get; set; } = new List(); + + /// + /// 只读列,模拟数据库计算列 + /// + [Display(Name = "只读列")] + public int ReadonlyColumn { get; init; } +} + +/// +/// +/// +public enum EnumEducation +{ + /// + /// + /// + [Display(Name = "小学")] + Primary, + + /// + /// + /// + [Display(Name = "中学")] + Middle +} diff --git a/test/UnitTestTcpSocket/SocketDataConverterCollectionsTest.cs b/test/UnitTestTcpSocket/SocketDataConverterCollectionsTest.cs new file mode 100644 index 00000000..0be74daa --- /dev/null +++ b/test/UnitTestTcpSocket/SocketDataConverterCollectionsTest.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace UnitTestTcpSocket; + +public class SocketDataConverterCollectionsTest +{ + [Fact] + public void TryGetConverter_Ok() + { + var sc = new ServiceCollection(); + sc.ConfigureDataConverters(options => + { + options.AddTypeConverter(); + options.AddPropertyConverter(entity => entity.Header, new DataPropertyConverterAttribute() + { + Offset = 0, + Length = 5 + }); + options.AddPropertyConverter(entity => entity.Body, new DataPropertyConverterAttribute() + { + Offset = 5, + Length = 2 + }); + + // 为提高代码覆盖率 重复添加转换器以后面的为准 + options.AddTypeConverter(); + options.AddPropertyConverter(entity => entity.Header, new DataPropertyConverterAttribute() + { + Offset = 0, + Length = 5 + }); + }); + + var provider = sc.BuildServiceProvider(); + var service = provider.GetRequiredService>(); + Assert.NotNull(service.Value); + + var ret = service.Value.TryGetTypeConverter(out var converter); + Assert.True(ret); + Assert.NotNull(converter); + + var fakeConverter = service.Value.TryGetTypeConverter(out var fooConverter); + Assert.False(fakeConverter); + Assert.Null(fooConverter); + + ret = service.Value.TryGetPropertyConverter(entity => entity.Header, out var propertyConverterAttribute); + Assert.True(ret); + Assert.NotNull(propertyConverterAttribute); + Assert.True(propertyConverterAttribute is { Offset: 0, Length: 5 }); + + ret = service.Value.TryGetPropertyConverter(entity => entity.Name, out var fooPropertyConverterAttribute); + Assert.False(ret); + Assert.Null(fooPropertyConverterAttribute); + + ret = service.Value.TryGetPropertyConverter(entity => entity.ToString(), out _); + Assert.False(ret); + } + + class MockEntity + { + public byte[]? Header { get; set; } + + public byte[]? Body { get; set; } + } + + class MockLoggerProvider : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) + { + return new MockLogger(); + } + + public void Dispose() + { + + } + } + + class MockLogger : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + + } + } +} diff --git a/test/UnitTestTcpSocket/TcpSocketFactoryTest.cs b/test/UnitTestTcpSocket/TcpSocketFactoryTest.cs new file mode 100644 index 00000000..76673d60 --- /dev/null +++ b/test/UnitTestTcpSocket/TcpSocketFactoryTest.cs @@ -0,0 +1,1283 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text; + +namespace UnitTestTcpSocket; + +public class TcpSocketFactoryTest +{ + [Fact] + public async Task GetOrCreate_Ok() + { + // 测试 GetOrCreate 方法创建的 Client 销毁后继续 GetOrCreate 得到的对象是否可用 + var sc = new ServiceCollection(); + sc.AddLogging(builder => + { + builder.AddProvider(new MockLoggerProvider()); + }); + sc.AddBootstrapBlazorTcpSocketFactory(); + var provider = sc.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + var client1 = factory.GetOrCreate("demo", op => op.LocalEndPoint = TcpSocketUtility.ConvertToIpEndPoint("localhost", 0)); + await client1.CloseAsync(); + + var client2 = factory.GetOrCreate("demo", op => op.LocalEndPoint = TcpSocketUtility.ConvertToIpEndPoint("localhost", 0)); + Assert.Equal(client1, client2); + + var ip = Dns.GetHostAddresses(Dns.GetHostName(), AddressFamily.InterNetwork).FirstOrDefault() ?? IPAddress.Loopback; + var client3 = factory.GetOrCreate("demo1", op => op.LocalEndPoint = TcpSocketUtility.ConvertToIpEndPoint(ip.ToString(), 0)); + + // 测试不合格 IP 地址 + var client4 = factory.GetOrCreate("demo2", op => op.LocalEndPoint = TcpSocketUtility.ConvertToIpEndPoint("256.0.0.1", 0)); + + var client5 = factory.Remove("demo2"); + Assert.Equal(client4, client5); + Assert.NotNull(client5); + + await client5.DisposeAsync(); + await factory.DisposeAsync(); + } + + [Fact] + public async Task ConnectAsync_Timeout() + { + var client = CreateClient(builder => + { + // 增加发送报错 MockSocket + builder.AddTransient(); + }); + client.Options.ConnectTimeout = 10; + + var connect = await client.ConnectAsync("localhost", 9999); + Assert.False(connect); + } + + [Fact] + public async Task ConnectAsync_Cancel() + { + var client = CreateClient(builder => + { + builder.AddTransient(); + }, + options => + { + options.ConnectTimeout = 500; + }); + + // 测试 ConnectAsync 方法连接取消逻辑 + var cst = new CancellationTokenSource(); + cst.Cancel(); + var connect = await client.ConnectAsync("localhost", 9999, cst.Token); + + // 由于信号量被取消,所以连接会失败 + Assert.False(connect); + + // 测试真正的连接被取消逻辑 + cst = new CancellationTokenSource(200); + connect = await client.ConnectAsync("localhost", 9999, cst.Token); + Assert.False(connect); + } + + [Fact] + public async Task ConnectAsync_Failed() + { + var client = CreateClient(); + + // 测试 ConnectAsync 方法连接失败 + var connect = await client.ConnectAsync("localhost", 9999); + Assert.False(connect); + } + + [Fact] + public async Task ConnectAsync_Error() + { + var client = CreateClient(); + + // 反射设置 SocketClientProvider 为空 + var propertyInfo = client.GetType().GetProperty("ServiceProvider", BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(propertyInfo); + propertyInfo.SetValue(client, null); + + // 测试 ConnectAsync 方法连接失败 + var ex = await Assert.ThrowsAsync(async () => await client.ConnectAsync("localhost", 9999)); + Assert.NotNull(ex); + + // 反射测试 Log 方法 + var methodInfo = client.GetType().GetMethod("Log", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(methodInfo); + methodInfo.Invoke(client, [LogLevel.Error, null!, "Test error log"]); + } + + [Fact] + public async Task ConnectAsync_Lock() + { + // 测试并发锁问题 + var provider = new MockAutoReconnectLockSocketProvider(); + var client = CreateClient(builder => + { + builder.AddTransient(p => provider); + }); + + // 开 5 个线程同时连接 + _ = Task.Run(async () => + { + // 延时 150 保证有一个连接失败 + await Task.Delay(150); + provider.SetConnected(true); + }); + var results = await Task.WhenAll(Enumerable.Range(1, 5).Select(i => client.ConnectAsync("localhost", 0).AsTask())); + // 期望结果是 1个 false 4个 true + Assert.Equal(1, results.Count(r => !r)); + Assert.Equal(4, results.Count(r => r)); + } + + [Fact] + public async Task Send_Timeout() + { + var port = 8887; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(builder => + { + // 增加发送报错 MockSocket + builder.AddTransient(); + }); + client.Options.SendTimeout = 10; + + await client.ConnectAsync("localhost", port); + + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + var result = await client.SendAsync(data); + Assert.False(result); + } + + [Fact] + public async Task SendAsync_Error() + { + var client = CreateClient(builder => + { + // 增加发送报错 MockSocket + builder.AddTransient(); + }); + + // 测试未建立连接前调用 SendAsync 方法报异常逻辑 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + var ex = await Assert.ThrowsAsync(async () => await client.SendAsync(data)); + Assert.NotNull(ex); + + // 测试发送失败 + var port = 8892; + var server = StartTcpServer(port, MockSplitPackageAsync); + + await client.ConnectAsync("localhost", port); + Assert.True(client.IsConnected); + + // 内部生成异常日志 + await client.SendAsync(data); + } + + [Fact] + public async Task SendAsync_Cancel() + { + var port = 8881; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(); + Assert.False(client.IsConnected); + + // 连接 TCP Server + await client.ConnectAsync("localhost", port); + Assert.True(client.IsConnected); + + // 测试 SendAsync 方法发送取消逻辑 + var cst = new CancellationTokenSource(); + cst.Cancel(); + + var result = await client.SendAsync("test", null, cst.Token); + Assert.False(result); + + result = await client.SendAsync("test", Encoding.UTF8, cst.Token); + Assert.False(result); + + // 关闭连接 + StopTcpServer(server); + } + + [Fact] + public async Task ReceiveAsync_Timeout() + { + var port = 8888; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(); + client.Options.ReceiveTimeout = 100; + + await client.ConnectAsync("localhost", port); + + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + await Task.Delay(220); // 等待接收超时 + } + + [Fact] + public async Task ReceiveAsync_Cancel() + { + var port = 8889; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(); + await client.ConnectAsync("localhost", port); + + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + // 通过反射取消令牌 + var type = client.GetType(); + Assert.NotNull(type); + + var fieldInfo = type.GetField("_receiveCancellationTokenSource", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(fieldInfo); + var tokenSource = fieldInfo.GetValue(client) as CancellationTokenSource; + Assert.NotNull(tokenSource); + tokenSource.Cancel(); + await Task.Delay(50); + } + + [Fact] + public async Task ReceiveAsync_InvalidOperationException() + { + // 未连接时调用 ReceiveAsync 方法会抛出 InvalidOperationException 异常 + var client = CreateClient(); + var ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); + Assert.NotNull(ex); + + // 已连接但是启用了自动接收功能时调用 ReceiveAsync 方法会抛出 InvalidOperationException 异常 + var port = 8893; + var server = StartTcpServer(port, MockSplitPackageAsync); + + client.Options.IsAutoReceive = true; + var connected = await client.ConnectAsync("localhost", port); + Assert.True(connected); + + ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); + Assert.NotNull(ex); + } + + [Fact] + public async Task ReceiveAsync_Ok() + { + var onConnecting = false; + var onConnected = false; + var port = 8891; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(); + client.Options.IsAutoReceive = false; + client.OnConnecting = () => + { + onConnecting = true; + return Task.CompletedTask; + }; + client.OnConnected = () => + { + onConnected = true; + return Task.CompletedTask; + }; + var connected = await client.ConnectAsync("localhost", port); + Assert.True(connected); + Assert.True(onConnecting); + Assert.True(onConnected); + + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + var send = await client.SendAsync(data); + Assert.True(send); + + // 未设置数据处理器未开启自动接收时,调用 ReceiveAsync 方法获取数据 + // 需要自己处理粘包分包和业务问题 + var payload = await client.ReceiveAsync(); + Assert.Equal([1, 2, 3, 4, 5], payload.ToArray()); + + // 由于服务器端模拟了拆包发送第二段数据,所以这里可以再次调用 ReceiveAsync 方法获取第二段数据 + payload = await client.ReceiveAsync(); + Assert.Equal([3, 4], payload.ToArray()); + } + + [Fact] + public async Task ReceiveAsync_Error() + { + var client = CreateClient(); + + // 测试未建立连接前调用 ReceiveAsync 方法报异常逻辑 + var type = client.GetType(); + Assert.NotNull(type); + + var methodInfo = type.GetMethod("AutoReceiveAsync", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(methodInfo); + + var task = (ValueTask)methodInfo.Invoke(client, null)!; + var ex = await Assert.ThrowsAsync(async () => await task); + Assert.NotNull(ex); + + var port = 8882; + var server = StartTcpServer(port, MockSplitPackageAsync); + + Assert.Equal(1024 * 64, client.Options.ReceiveBufferSize); + + client.Options.ReceiveBufferSize = 1024 * 20; + Assert.Equal(1024 * 20, client.Options.ReceiveBufferSize); + + ReadOnlyMemory buffer = ReadOnlyMemory.Empty; + var tcs = new TaskCompletionSource(); + + // 增加接收回调方法 + client.ReceivedCallBack = b => + { + buffer = b; + tcs.SetResult(); + return ValueTask.CompletedTask; + }; + + await client.ConnectAsync("localhost", port); + + // 发送数据导致接收数据异常 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + await tcs.Task; + Assert.Equal([1, 2, 3, 4, 5], buffer.ToArray()); + + // 关闭连接 + StopTcpServer(server); + } + + [Fact] + public async Task AutoReconnect_Ok() + { + var client = CreateClient(optionConfigure: options => + { + options.IsAutoReconnect = true; + options.ReconnectInterval = 200; + options.IsAutoReceive = true; + }); + + // 使用场景自动接收数据,短线后自动重连 + var port = 8894; + var connect = await client.ConnectAsync("localhost", port); + Assert.False(connect); + + // 开启服务端后,可以自动重连上 + var server = StartTcpServer(port, LoopSendPackageAsync); + await Task.Delay(250); + Assert.True(client.IsConnected); + + await client.DisposeAsync(); + } + + [Fact] + public async Task AutoReconnect_False() + { + var provider = new MockAutoReconnectSocketProvider(); + var client = CreateClient(builder => + { + // 增加发送报错 MockSocket + builder.AddTransient(p => provider); + }, + optionConfigure: options => + { + options.IsAutoReconnect = true; + options.ReconnectInterval = 200; + options.IsAutoReceive = true; + }); + + // 使用场景自动接收数据,短线后自动重连 + var connect = await client.ConnectAsync("localhost", 0); + Assert.False(connect); + + provider.SetConnected(true); + await Task.Delay(250); + Assert.True(client.IsConnected); + } + + [Fact] + public async Task AutoReconnect_Send_Ok() + { + // 发送数据时连接断开了,测试重连功能 + var provider = new MockAutoReconnectSocketProvider(); + var client = CreateClient(builder => + { + // 增加发送报错 MockSocket + builder.AddTransient(p => provider); + }, optionConfigure: options => + { + options.IsAutoReconnect = true; + options.ReconnectInterval = 200; + options.IsAutoReceive = true; + }); + + provider.SetConnected(true); + var connect = await client.ConnectAsync("localhost", 0); + Assert.True(connect); + + // 发送时断开连接 + provider.SetSend(false); + var send = await client.SendAsync("test"); + Assert.False(send); + + await Task.Delay(250); + Assert.True(client.IsConnected); + } + + [Fact] + public async Task AutoReconnect_Receive_Ok() + { + // 接收数据时连接断开了,测试重连功能 + var provider = new MockAutoReconnectSocketProvider(); + var client = CreateClient(builder => + { + // 增加发送报错 MockSocket + builder.AddTransient(p => provider); + }, optionConfigure: options => + { + options.IsAutoReconnect = true; + options.ReconnectInterval = 200; + options.IsAutoReceive = false; + }); + + provider.SetConnected(true); + var connect = await client.ConnectAsync("localhost", 0); + Assert.True(connect); + + // 发送时断开连接 + provider.SetReceive(false); + var buffer = await client.ReceiveAsync(); + Assert.Equal(Memory.Empty, buffer); + + await Task.Delay(250); + provider.SetReceive(true); + buffer = await client.ReceiveAsync(); + Assert.Equal(5, buffer.Length); + } + + [Fact] + public async Task AutoReconnect_Cancel() + { + // 测试重连时取消逻辑 + var provider = new MockAutoReconnectSocketProvider(); + var client = CreateClient(builder => + { + // 增加发送报错 MockSocket + builder.AddTransient(p => provider); + }, optionConfigure: options => + { + options.IsAutoReconnect = true; + options.ReconnectInterval = 2000; + options.IsAutoReceive = false; + }); + + await client.ConnectAsync("localhost", 0); + await Task.Delay(100); + await client.DisposeAsync(); + } + + [Fact] + public async Task FixLengthDataPackageHandler_Ok() + { + var port = 8884; + var server = StartTcpServer(port, MockSplitPackageAsync); + var client = CreateClient(); + var tcs = new TaskCompletionSource(); + var receivedBuffer = new byte[1024]; + + // 设置数据适配器 + var adapter = new DataPackageAdapter + { + DataPackageHandler = new FixLengthDataPackageHandler(7) + }; + client.SetDataPackageAdapter(adapter, buffer => + { + // buffer 即是接收到的数据 + buffer.CopyTo(receivedBuffer); + receivedBuffer = receivedBuffer[..buffer.Length]; + tcs.SetResult(); + return ValueTask.CompletedTask; + }); + + // 测试 ConnectAsync 方法 + var connect = await client.ConnectAsync("localhost", port); + Assert.True(connect); + Assert.True(client.IsConnected); + + // 测试 SendAsync 方法 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + var result = await client.SendAsync(data); + Assert.True(result); + + await tcs.Task; + Assert.Equal([1, 2, 3, 4, 5, 3, 4], receivedBuffer.ToArray()); + + // 关闭连接 + await client.CloseAsync(); + StopTcpServer(server); + } + + [Fact] + public async Task FixLengthDataPackageHandler_Sticky() + { + var port = 8885; + var server = StartTcpServer(port, MockStickyPackageAsync); + var client = CreateClient(); + var tcs = new TaskCompletionSource(); + var receivedBuffer = new byte[128]; + + // 连接 TCP Server + var connect = await client.ConnectAsync("localhost", port); + + // 设置数据适配器 + var adapter = new DataPackageAdapter + { + DataPackageHandler = new FixLengthDataPackageHandler(7) + }; + + client.SetDataPackageAdapter(adapter, buffer => + { + // buffer 即是接收到的数据 + buffer.CopyTo(receivedBuffer); + receivedBuffer = receivedBuffer[..buffer.Length]; + tcs.SetResult(); + return ValueTask.CompletedTask; + }); + + // 发送数据 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + // 等待接收数据处理完成 + await tcs.Task; + + // 验证接收到的数据 + Assert.Equal([1, 2, 3, 4, 5, 3, 4], receivedBuffer.ToArray()); + + // 重置接收缓冲区 + receivedBuffer = new byte[1024]; + tcs = new TaskCompletionSource(); + + // 等待第二次数据 + await tcs.Task; + + // 验证第二次收到的数据 + Assert.Equal([2, 2, 3, 4, 5, 6, 7], receivedBuffer.ToArray()); + tcs = new TaskCompletionSource(); + await tcs.Task; + + // 验证第三次收到的数据 + Assert.Equal([3, 2, 3, 4, 5, 6, 7], receivedBuffer.ToArray()); + + // 关闭连接 + await client.CloseAsync(); + StopTcpServer(server); + } + + [Fact] + public async Task DelimiterDataPackageHandler_Ok() + { + var port = 8883; + var server = StartTcpServer(port, MockDelimiterPackageAsync); + var client = CreateClient(); + var tcs = new TaskCompletionSource(); + var receivedBuffer = new byte[128]; + + // 设置数据适配器 + var adapter = new DataPackageAdapter + { + DataPackageHandler = new DelimiterDataPackageHandler([13, 10]), + }; + client.SetDataPackageAdapter(adapter, buffer => + { + // buffer 即是接收到的数据 + buffer.CopyTo(receivedBuffer); + receivedBuffer = receivedBuffer[..buffer.Length]; + tcs.SetResult(); + return ValueTask.CompletedTask; + }); + + // 连接 TCP Server + var connect = await client.ConnectAsync("localhost", port); + + // 发送数据 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + // 等待接收数据处理完成 + await tcs.Task; + + // 验证接收到的数据 + Assert.Equal([1, 2, 3, 4, 5, 13, 10], receivedBuffer.ToArray()); + + // 等待第二次数据 + receivedBuffer = new byte[1024]; + tcs = new TaskCompletionSource(); + await tcs.Task; + + // 验证接收到的数据 + Assert.Equal([5, 6, 13, 10], receivedBuffer.ToArray()); + + // 关闭连接 + await client.CloseAsync(); + StopTcpServer(server); + + var handler = new DelimiterDataPackageHandler("\r\n"); + var ex = Assert.Throws(() => new DelimiterDataPackageHandler(string.Empty)); + Assert.NotNull(ex); + + ex = Assert.Throws(() => new DelimiterDataPackageHandler(null!)); + Assert.NotNull(ex); + } + + [Fact] + public async Task TryConvertTo_Ok() + { + var port = 8886; + var server = StartTcpServer(port, MockEntityPackageAsync); + var client = CreateClient(); + var tcs = new TaskCompletionSource(); + MockEntity? entity = null; + + // 设置数据适配器 + var adapter = new DataPackageAdapter + { + DataPackageHandler = new FixLengthDataPackageHandler(29), + }; + client.SetDataPackageAdapter(adapter, new DataConverter(), t => + { + entity = t; + tcs.SetResult(); + return Task.CompletedTask; + }); + + // 连接 TCP Server + var connect = await client.ConnectAsync("localhost", port); + + // 发送数据 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + await tcs.Task; + + Assert.NotNull(entity); + Assert.Equal([1, 2, 3, 4, 5], entity.Header); + Assert.Equal([3, 4], entity.Body); + + // string + Assert.Equal("1", entity.Value1); + + // string + Assert.Equal("1", entity.Value14); + + // int + Assert.Equal(9, entity.Value2); + + // long + Assert.Equal(16, entity.Value3); + + // double + Assert.Equal(3.14, entity.Value4); + + // single + Assert.NotEqual(0, entity.Value5); + + // short + Assert.Equal(0x23, entity.Value6); + + // ushort + Assert.Equal(0x24, entity.Value7); + + // uint + Assert.Equal((uint)0x25, entity.Value8); + + // ulong + Assert.Equal((ulong)0x26, entity.Value9); + + // bool + Assert.True(entity.Value10); + + // enum + Assert.Equal(EnumEducation.Middle, entity.Value11); + + // foo + Assert.NotNull(entity.Value12); + Assert.Equal(0x29, entity.Value12.Id); + Assert.Equal("test", entity.Value12.Name); + + // no attribute + Assert.Null(entity.Value13); + + // 测试 SocketDataConverter 标签功能 + tcs = new TaskCompletionSource(); + client.SetDataPackageAdapter(adapter, t => + { + entity = t; + tcs.SetResult(); + return Task.CompletedTask; + }); + await client.SendAsync(data); + await tcs.Task; + + Assert.NotNull(entity); + Assert.Equal([1, 2, 3, 4, 5], entity.Header); + + // 测试数据适配器直接调用 TryConvertTo 方法转换数据 + var adapter2 = new DataPackageAdapter(); + var result = adapter2.TryConvertTo(data, new DataConverter(), out var t); + Assert.True(result); + Assert.NotNull(t); + Assert.Equal([1, 2, 3, 4, 5], entity.Header); + + // 测试 SetDataPackageAdapter 泛型无标签情况 + tcs = new TaskCompletionSource(); + NoConvertEntity? noConvertEntity = null; + client.SetDataPackageAdapter(adapter, t => + { + noConvertEntity = t; + tcs.SetResult(); + return Task.CompletedTask; + }); + await client.SendAsync(data); + await tcs.Task; + Assert.Null(noConvertEntity); + + var converter = new MockSocketDataConverter(); + result = converter.TryConvertTo(new byte[] { 0x1, 0x2 }, out t); + Assert.False(result); + + server.Stop(); + } + + [Fact] + public async Task TryGetTypeConverter_Ok() + { + // 测试服务配置转换器 + var port = 8895; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(builder => + { + builder.Configure(options => + { + options.AddTypeConverter(); + options.AddPropertyConverter(entity => entity.Header, new DataPropertyConverterAttribute() + { + Offset = 0, + Length = 5 + }); + options.AddPropertyConverter(entity => entity.Body, new DataPropertyConverterAttribute() + { + Offset = 5, + Length = 2 + }); + }); + }); + var tcs = new TaskCompletionSource(); + var receivedBuffer = new byte[128]; + + // 连接 TCP Server + var connect = await client.ConnectAsync("localhost", port); + + // 设置数据适配器 + var adapter = new DataPackageAdapter + { + DataPackageHandler = new FixLengthDataPackageHandler(7) + }; + + OptionConvertEntity? entity = null; + client.SetDataPackageAdapter(adapter, data => + { + // buffer 即是接收到的数据 + entity = data; + tcs.SetResult(); + return Task.CompletedTask; + }); + + // 发送数据 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + // 等待接收数据处理完成 + await tcs.Task; + Assert.NotNull(entity); + Assert.Equal([1, 2, 3, 4, 5], entity.Header); + Assert.Equal([3, 4], entity.Body); + + server.Stop(); + } + + private static TcpListener StartTcpServer(int port, Func handler) + { + var server = new TcpListener(IPAddress.Loopback, port); + server.Start(); + Task.Run(() => AcceptClientsAsync(server, handler)); + return server; + } + + private static async Task AcceptClientsAsync(TcpListener server, Func handler) + { + while (true) + { + var client = await server.AcceptTcpClientAsync(); + _ = Task.Run(() => handler(client)); + } + } + + private static async Task MockDelimiterPackageAsync(TcpClient client) + { + using var stream = client.GetStream(); + while (true) + { + var buffer = new byte[10240]; + var len = await stream.ReadAsync(buffer); + if (len == 0) + { + break; + } + + // 回写数据到客户端 + var block = new ReadOnlyMemory(buffer, 0, len); + await stream.WriteAsync(block, CancellationToken.None); + + await Task.Delay(20); + + // 模拟拆包发送第二段数据 + await stream.WriteAsync(new byte[] { 13, 10, 0x5, 0x6, 13, 10 }, CancellationToken.None); + } + } + + private static async Task MockSplitPackageAsync(TcpClient client) + { + using var stream = client.GetStream(); + while (true) + { + var buffer = new byte[1024]; + var len = await stream.ReadAsync(buffer); + if (len == 0) + { + break; + } + + // 回写数据到客户端 + var block = new ReadOnlyMemory(buffer, 0, len); + await stream.WriteAsync(block, CancellationToken.None); + + // 模拟延时 + await Task.Delay(50); + + // 模拟拆包发送第二段数据 + await stream.WriteAsync(new byte[] { 0x3, 0x4 }, CancellationToken.None); + } + } + + private static async Task MockEntityPackageAsync(TcpClient client) + { + using var stream = client.GetStream(); + while (true) + { + var buffer = new byte[1024]; + var len = await stream.ReadAsync(buffer); + if (len == 0) + { + break; + } + + // 回写数据到客户端 + await stream.WriteAsync(new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5, 0x3, 0x4, 0x31, 0x09, 0x10, 0x40, 0x09, 0x1E, 0xB8, 0x51, 0xEB, 0x85, 0x1F, 0x40, 0x49, 0x0F, 0xDB, 0x23, 0x24, 0x25, 0x26, 0x01, 0x01, 0x29 }, CancellationToken.None); + } + } + + private static async Task MockStickyPackageAsync(TcpClient client) + { + using var stream = client.GetStream(); + while (true) + { + var buffer = new byte[10240]; + var len = await stream.ReadAsync(buffer); + if (len == 0) + { + break; + } + + // 回写数据到客户端 + var block = new ReadOnlyMemory(buffer, 0, len); + await stream.WriteAsync(block, CancellationToken.None); + + // 模拟延时 + await Task.Delay(10); + + // 模拟拆包发送第二段数据 + await stream.WriteAsync(new byte[] { 0x3, 0x4, 0x2, 0x2 }, CancellationToken.None); + + // 模拟延时 + await Task.Delay(10); + + // 模拟粘包发送后续数据 + await stream.WriteAsync(new byte[] { 0x3, 0x4, 0x5, 0x6, 0x7, 0x3, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x1 }, CancellationToken.None); + } + } + + private static async Task LoopSendPackageAsync(TcpClient client) + { + using var stream = client.GetStream(); + while (true) + { + // 模拟发送数据 + var data = new byte[] { 1, 2, 3, 4, 5 }; + await stream.WriteAsync(data, CancellationToken.None); + // 模拟延时 + await Task.Delay(500); + } + } + + private static void StopTcpServer(TcpListener server) + { + server?.Stop(); + } + + private static ITcpSocketClient CreateClient(Action? builder = null, Action? optionConfigure = null) + { + var sc = new ServiceCollection(); + sc.AddLogging(builder => + { + builder.AddProvider(new MockLoggerProvider()); + }); + sc.AddBootstrapBlazorTcpSocketFactory(); + builder?.Invoke(sc); + + var provider = sc.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + var client = factory.GetOrCreate("test", op => + { + op.LocalEndPoint = TcpSocketUtility.ConvertToIpEndPoint("localhost", 0); + op.EnableLog = true; + optionConfigure?.Invoke(op); + }); + return client; + } + + class MockLoggerProvider : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) + { + return new MockLogger(); + } + + public void Dispose() + { + + } + } + + class MockLogger : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + + } + } + + class MockSendErrorSocketProvider : ITcpSocketClientProvider + { + public bool IsConnected { get; private set; } + + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); + + public ValueTask CloseAsync() + { + return ValueTask.CompletedTask; + } + + public ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + IsConnected = true; + return ValueTask.FromResult(true); + } + + public ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default) + { + return ValueTask.FromResult(0); + } + + public ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + throw new Exception("Mock send error"); + } + } + + class MockConnectTimeoutSocketProvider : ITcpSocketClientProvider + { + public bool IsConnected { get; private set; } + + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); + + public ValueTask CloseAsync() + { + return ValueTask.CompletedTask; + } + + public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + await Task.Delay(1000, token); + IsConnected = false; + return false; + } + + public ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default) + { + return ValueTask.FromResult(0); + } + + public ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + return ValueTask.FromResult(true); + } + } + + class MockConnectCancelSocketProvider : ITcpSocketClientProvider + { + public bool IsConnected { get; private set; } + + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); + + public ValueTask CloseAsync() + { + return ValueTask.CompletedTask; + } + + public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + await Task.Delay(250, token); + return false; + } + + public ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default) + { + return ValueTask.FromResult(0); + } + + public ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + return ValueTask.FromResult(true); + } + } + + class MockSendTimeoutSocketProvider : ITcpSocketClientProvider + { + public bool IsConnected { get; private set; } + + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); + + public ValueTask CloseAsync() + { + return ValueTask.CompletedTask; + } + + public ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + IsConnected = true; + return ValueTask.FromResult(true); + } + + public ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default) + { + return ValueTask.FromResult(0); + } + + public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + // 模拟超时发送 + await Task.Delay(100, token); + return false; + } + } + + class MockAutoReconnectLockSocketProvider : ITcpSocketClientProvider + { + public bool IsConnected { get; private set; } + + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Loopback, 0); + + public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + await Task.Delay(100, token); + return IsConnected; + } + + public ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + return ValueTask.FromResult(true); + } + + public ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default) + { + byte[] data = [1, 2, 3, 4, 5]; + data.CopyTo(buffer); + return ValueTask.FromResult(5); + } + + public ValueTask CloseAsync() + { + return ValueTask.CompletedTask; + } + + public void SetConnected(bool state) + { + IsConnected = state; + } + } + + class MockAutoReconnectSocketProvider : ITcpSocketClientProvider + { + public bool IsConnected { get; private set; } + + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Loopback, 0); + + public ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + return ValueTask.FromResult(IsConnected); + } + + private bool _sendState = true; + public ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + return ValueTask.FromResult(_sendState); + } + + private bool _receiveState = true; + public ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default) + { + if (_receiveState) + { + byte[] data = [1, 2, 3, 4, 5]; + data.CopyTo(buffer); + return ValueTask.FromResult(5); + } + else + { + return ValueTask.FromResult(0); + } + } + + public ValueTask CloseAsync() + { + return ValueTask.CompletedTask; + } + + public void SetConnected(bool state) + { + IsConnected = state; + } + + public void SetSend(bool state) + { + _sendState = state; + } + + public void SetReceive(bool state) + { + _receiveState = state; + } + } + + [DataTypeConverter(Type = typeof(DataConverter))] + class MockEntity + { + [DataPropertyConverter(Type = typeof(byte[]), Offset = 0, Length = 5)] + public byte[]? Header { get; set; } + + [DataPropertyConverter(Type = typeof(byte[]), Offset = 5, Length = 2)] + public byte[]? Body { get; set; } + + [DataPropertyConverter(Type = typeof(string), Offset = 7, Length = 1, EncodingName = "utf-8")] + public string? Value1 { get; set; } + + [DataPropertyConverter(Type = typeof(int), Offset = 8, Length = 1)] + public int Value2 { get; set; } + + [DataPropertyConverter(Type = typeof(long), Offset = 9, Length = 1)] + public long Value3 { get; set; } + + [DataPropertyConverter(Type = typeof(double), Offset = 10, Length = 8)] + public double Value4 { get; set; } + + [DataPropertyConverter(Type = typeof(float), Offset = 18, Length = 4)] + public float Value5 { get; set; } + + [DataPropertyConverter(Type = typeof(short), Offset = 22, Length = 1)] + public short Value6 { get; set; } + + [DataPropertyConverter(Type = typeof(ushort), Offset = 23, Length = 1)] + public ushort Value7 { get; set; } + + [DataPropertyConverter(Type = typeof(uint), Offset = 24, Length = 1)] + public uint Value8 { get; set; } + + [DataPropertyConverter(Type = typeof(ulong), Offset = 25, Length = 1)] + public ulong Value9 { get; set; } + + [DataPropertyConverter(Type = typeof(bool), Offset = 26, Length = 1)] + public bool Value10 { get; set; } + + [DataPropertyConverter(Type = typeof(EnumEducation), Offset = 27, Length = 1)] + public EnumEducation Value11 { get; set; } + + [DataPropertyConverter(Type = typeof(Foo), Offset = 28, Length = 1, ConverterType = typeof(FooConverter), ConverterParameters = ["test"])] + public Foo? Value12 { get; set; } + + [DataPropertyConverter(Type = typeof(string), Offset = 7, Length = 1)] + public string? Value14 { get; set; } + + public string? Value13 { get; set; } + } + + class MockSocketDataConverter : DataConverter + { + protected override bool Parse(ReadOnlyMemory data, MockEntity entity) + { + return false; + } + } + + class FooConverter(string name) : IDataPropertyConverter + { + public object? Convert(ReadOnlyMemory data) + { + return new Foo() { Id = data.Span[0], Name = name }; + } + } + + class NoConvertEntity + { + public byte[]? Header { get; set; } + + public byte[]? Body { get; set; } + } + + class OptionConvertEntity + { + public byte[]? Header { get; set; } + + public byte[]? Body { get; set; } + } +} diff --git a/test/UnitTestTcpSocket/TcpSocketPropertyConverterTest.cs b/test/UnitTestTcpSocket/TcpSocketPropertyConverterTest.cs new file mode 100644 index 00000000..cfd3efc0 --- /dev/null +++ b/test/UnitTestTcpSocket/TcpSocketPropertyConverterTest.cs @@ -0,0 +1,72 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace UnitTestTcpSocket; + +public class TcpSocketPropertyConverterTest +{ + [Fact] + public void UInt16Converter_Ok() + { + var converter = new DataUInt16LittleEndianConverter(); + var actual = converter.Convert(new byte[] { 0xFF, 0x00 }); + Assert.Equal((ushort)0xFF, actual); + } + + [Fact] + public void Int16Converter_Ok() + { + var converter = new DataInt16LittleEndianConverter(); + var actual = converter.Convert(new byte[] { 0xFF, 0x00 }); + Assert.Equal((short)0xFF, actual); + } + + [Fact] + public void UInt32Converter_Ok() + { + var converter = new DataUInt32LittleEndianConverter(); + var actual = converter.Convert(new byte[] { 0xFF, 0x00, 0x00, 0x00 }); + Assert.Equal((uint)0xFF, actual); + } + + [Fact] + public void Int32Converter_Ok() + { + var converter = new DataInt32LittleEndianConverter(); + var actual = converter.Convert(new byte[] { 0xFF, 0x00, 0x00, 0x00 }); + Assert.Equal(0xFF, actual); + } + + [Fact] + public void UInt64Converter_Ok() + { + var converter = new DataUInt64LittleEndianConverter(); + var actual = converter.Convert(new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); + Assert.Equal((ulong)0xFF, actual); + } + + [Fact] + public void Int64Converter_Ok() + { + var converter = new DataInt64LittleEndianConverter(); + var actual = converter.Convert(new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); + Assert.Equal((long)0xFF, actual); + } + + [Fact] + public void SingleConverter_Ok() + { + var converter = new DataSingleLittleEndianConverter(); + var actual = converter.Convert(new byte[] { 0xC3, 0xF5, 0x48, 0x40 }); + Assert.Equal((float)3.14, actual); + } + + [Fact] + public void DoubleConverter_Ok() + { + var converter = new DataDoubleLittleEndianConverter(); + var actual = converter.Convert(new byte[] { 0x1F, 0x85, 0xEB, 0x51, 0xB8, 0x1E, 0x09, 0x40 }); + Assert.Equal((double)3.14, actual); + } +} diff --git a/test/UnitTestTcpSocket/TcpSocketUtiityTest.cs b/test/UnitTestTcpSocket/TcpSocketUtiityTest.cs new file mode 100644 index 00000000..1f259696 --- /dev/null +++ b/test/UnitTestTcpSocket/TcpSocketUtiityTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Net; + +namespace UnitTestTcpSocket; + +public class TcpSocketUtiityTest +{ + + [Fact] + public void ConvertToIPAddress_Ok() + { + var ex = Assert.Throws(() => TcpSocketUtility.ConvertToIPAddress("")); + Assert.NotNull(ex); + + var address = TcpSocketUtility.ConvertToIPAddress("any"); + Assert.Equal(IPAddress.Any, address); + } + + [Fact] + public void ConvertToIpEndPoint_Ok() + { + var ex = Assert.Throws(() => TcpSocketUtility.ConvertToIpEndPoint("localhost", 88990)); + Assert.NotNull(ex); + + ex = null; + ex = Assert.Throws(() => TcpSocketUtility.ConvertToIpEndPoint("localhost", -1000)); + Assert.NotNull(ex); + } +} diff --git a/test/UnitTestTcpSocket/UnitTestTcpSocket.csproj b/test/UnitTestTcpSocket/UnitTestTcpSocket.csproj new file mode 100644 index 00000000..3a0e5dc4 --- /dev/null +++ b/test/UnitTestTcpSocket/UnitTestTcpSocket.csproj @@ -0,0 +1,29 @@ + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + +