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