From b36bcf543d90a69de58757be93bb7224baa7fc2d Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 19 Jul 2025 11:34:55 +0800 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=94=B9=20SocketData?= =?UTF-8?q?Converter=20=E8=99=9A=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...aConverterBase.cs => SocketDataConverter.cs} | 16 ++++++++++++++-- test/UnitTest/Services/TcpSocketFactoryTest.cs | 17 +++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) rename src/BootstrapBlazor/Services/TcpSocket/DataConverter/{SocketDataConverterBase.cs => SocketDataConverter.cs} (75%) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataConverter/SocketDataConverterBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataConverter/SocketDataConverter.cs similarity index 75% rename from src/BootstrapBlazor/Services/TcpSocket/DataConverter/SocketDataConverterBase.cs rename to src/BootstrapBlazor/Services/TcpSocket/DataConverter/SocketDataConverter.cs index eaff156bfbc..642cac45870 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataConverter/SocketDataConverterBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataConverter/SocketDataConverter.cs @@ -11,7 +11,7 @@ namespace BootstrapBlazor.Components; /// Provides a base class for converting socket data into a specified entity type. /// /// The type of entity to convert the socket data into. -public abstract class SocketDataConverterBase : ISocketDataConverter +public class SocketDataConverter : ISocketDataConverter { /// /// @@ -19,7 +19,19 @@ public abstract class SocketDataConverterBase : ISocketDataConverter /// /// - public abstract bool TryConvertTo(ReadOnlyMemory data, [NotNullWhen(true)] out TEntity? entity); + 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(); /// /// 将字节数据转换为指定实体类型的实例。 diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index d9c977c9108..e6b8e66124e 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -655,7 +655,7 @@ public async Task TryConvertTo_Ok() { DataPackageHandler = new FixLengthDataPackageHandler(29), }; - client.SetDataPackageAdapter(adapter, new MockEntitySocketDataConverter(), t => + client.SetDataPackageAdapter(adapter, new SocketDataConverter(), t => { entity = t; tcs.SetResult(); @@ -734,7 +734,7 @@ public async Task TryConvertTo_Ok() // 测试数据适配器直接调用 TryConvertTo 方法转换数据 var adapter2 = new DataPackageAdapter(); - var result = adapter2.TryConvertTo(data, new MockEntitySocketDataConverter(), out var t); + var result = adapter2.TryConvertTo(data, new SocketDataConverter(), out var t); Assert.True(result); Assert.NotNull(t); Assert.Equal([1, 2, 3, 4, 5], entity.Header); @@ -1137,18 +1137,7 @@ public void SetReceive(bool state) } } - class MockEntitySocketDataConverter : SocketDataConverterBase - { - public override bool TryConvertTo(ReadOnlyMemory data, [NotNullWhen(true)] out MockEntity? entity) - { - var v = new MockEntity(); - var ret = Parse(data, v); - entity = ret ? v : null; - return ret; - } - } - - [SocketDataConverter(Type = typeof(MockEntitySocketDataConverter))] + [SocketDataConverter(Type = typeof(SocketDataConverter))] class MockEntity { [SocketDataProperty(Type = typeof(byte[]), Offset = 0, Length = 5)] From 35f8b933f74fddc0e3a6c28a47fe3e01d01db6fa Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 19 Jul 2025 11:35:55 +0800 Subject: [PATCH 2/4] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/SocketClientOptions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/SocketClientOptions.cs b/src/BootstrapBlazor/Services/TcpSocket/SocketClientOptions.cs index 726c712d5a3..d6d525c4fbf 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/SocketClientOptions.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/SocketClientOptions.cs @@ -10,7 +10,7 @@ namespace BootstrapBlazor.Components; /// /// 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, +/// 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 SocketClientOptions @@ -49,14 +49,14 @@ public class SocketClientOptions /// 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 IPEndPoint(IPAddress.Any, 0); + 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; } From 8aab5348d996820b1d771815f8488bf1a5290dcb Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 19 Jul 2025 11:36:15 +0800 Subject: [PATCH 3/4] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/SocketFactories.razor | 47 +++++++++++++++---- .../Components/Samples/Sockets/Adapters.razor | 4 +- .../Samples/Sockets/Adapters.razor.cs | 6 +-- .../Samples/Sockets/DataEntities.razor | 12 ++--- src/BootstrapBlazor.Server/Locales/zh-CN.json | 2 +- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor b/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor index 7e220d4476b..e06b1682265 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor @@ -54,16 +54,15 @@ private async Task CreateClient() // 创建 ITcpSocketClient 实例 var client = TcpSocketFactory.GetOrCreate("localhost", 0); - // 设置 FixLengthDataPackageHandler 数据处理器处理数据定长 4 的数据 - client.SetDataHandler(new FixLengthDataPackageHandler(4) + // 设置数据适配器 使用 FixLengthDataPackageHandler 数据处理器处理数据定长 4 的数据 + var adapter = new DataPackageAdapter { - // 设置接收数据回调方法 - ReceivedCallBack = buffer => - { - // 内部自动处理粘包分包问题,这里参数 buffer 一定是定长为 4 的业务数据 - receivedBuffer = buffer; - return Task.CompletedTask; - } + DataPackageHandler = new FixLengthDataPackageHandler(4) + }; + client.SetDataPackageAdapter(adapter, buffer => + { + // buffer 即是接收到的数据 + return ValueTask.CompletedTask; }); // 连接远端节点 连接成功后自动开始接收数据 @@ -78,8 +77,36 @@ private async Task CreateClient()
  • DelimiterDataPackageHandler 分隔符数据处理器 即通讯包以特定一个或一组字节分割
  • -

    5. 数据适配器(设计中)

    +

    5. 数据适配器

    在我们实际应用中,接收到数据包后(已经过数据处理器)大多情况下是需要将电文转化为应用中的具体数据类型 ClassStruct。将原始数据包转化为类或者结构体的过程由我们的数据适配器来实现

    数据适配器设计思路如下

    + +
      +
    1. 使用 SocketDataConverterAttribute 标签约定通讯数据使用那个转换类型进行转换 指定类型需继承 ISocketDataConverter + 接口 +
    2. +
    3. 使用 SocketDataPropertyAttribute 标签约定如何转换数据类型 (Property) 属性值
    4. +
    + +
    [SocketDataConverter(Type = typeof(SocketDataConverter<MockEntity>))]
    +class MockEntity
    +{
    +    [SocketDataProperty(Type = typeof(byte[]), Offset = 0, Length = 5)]
    +    public byte[]? Header { get; set; }
    +
    +    [SocketDataProperty(Type = typeof(byte[]), Offset = 5, Length = 2)]
    +    public byte[]? Body { get; set; }
    +
    +    [SocketDataProperty(Type = typeof(Foo), Offset = 7, Length = 1, ConverterType = typeof(FooConverter), ConverterParameters = ["test"])]
    +    public string? Value1 { get; set; }
    +}
    + +
    class FooConverter(string name) : ISocketDataPropertyConverter
    +{
    +    public object? Convert(ReadOnlyMemory<byte> data)
    +    {
    +        return new Foo() { Id = data.Span[0], Name = name };
    +    }
    +}
    diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor index e1402fcef03..77df9c61d3f 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor @@ -41,8 +41,8 @@

    本示例服务器端模拟了数据分包即响应数据实际是两次写入所以实际接收端是要通过两次接收才能得到一个完整的响应数据包,可通过 数据适配器 来简化接收逻辑。通过切换下方 是否使用数据适配器 控制开关进行测试查看实际数据接收情况。

      -
    • 不使用 数据处理器 要分两次接收才能接收完整
    • -
    • 使用 数据处理器 一次即可接收完整数据包
    • +
    • 不使用 数据适配器 要分两次接收才能接收完整
    • +
    • 使用 数据适配器 一次即可接收完整数据包
    private readonly DataPackageAdapter _dataAdapter = new()
     {
    diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs
    index 9125c8f7978..b8871f39141 100644
    --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs
    +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs
    @@ -48,10 +48,10 @@ protected override void OnInitialized()
             });
             _client.ReceivedCallBack += OnReceivedAsync;
     
    -        _dataAdapter.ReceivedCallBack += async Data =>
    +        _dataAdapter.ReceivedCallBack += async data =>
             {
                 // 直接处理接收的数据
    -            await UpdateReceiveLog(Data);
    +            await UpdateReceiveLog(data);
             };
         }
     
    @@ -104,7 +104,7 @@ private async ValueTask OnReceivedAsync(ReadOnlyMemory data)
     
         private async Task UpdateReceiveLog(ReadOnlyMemory data)
         {
    -        var payload = System.Text.Encoding.UTF8.GetString(data.Span);
    +        var payload = Encoding.UTF8.GetString(data.Span);
             var body = BitConverter.ToString(data.ToArray());
     
             _items.Add(new ConsoleMessageItem
    diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/DataEntities.razor b/src/BootstrapBlazor.Server/Components/Samples/Sockets/DataEntities.razor
    index e911c693677..abc9669f65a 100644
    --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/DataEntities.razor
    +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/DataEntities.razor
    @@ -9,7 +9,7 @@
                Name="Normal" ShowCode="false">
         

    通过 SocketDataConverterAttribute 类标签与 SocketDataPropertyAttribute 属性标签可以将通讯数据自动转化为我们系统中需要的业务实体类,示例如下:

    -
    [SocketDataConverter(Type = typeof(MockEntitySocketDataConverter))]
    +    
    [SocketDataConverter(Type = typeof(SocketDataConverter<MockEntity>))]
     class MockEntity
     {
         [SocketDataProperty(Type = typeof(byte[]), Offset = 0, Length = 5)]
    @@ -26,8 +26,7 @@ class MockEntity
     }

    1. SocketDataConverter 参数说明:

      -
    • Type: 自定义转换器类型,组件库内置了 SocketDataConverterBase 泛型基类,继承后非常方便扩展出自己的转换器,重载 - TryConvertTo 方法调用基类的 Parse 基本就完成了 99% 的工作量,也可以完全自己解析通讯电文到实体类的各个属性上 +
    • Type: 自定义转换器类型,组件库内置了 SocketDataConverter 泛型类,建议看一下源码非常方便扩展出自己的转换器
    class MockEntitySocketDataConverter : SocketDataConverterBase<MockEntity>
    @@ -96,7 +95,7 @@ class MockEntity
             
  • SocketDataUInt64LittleEndianConverter 转成 ulong 无符号整形小端读取
  • 自定义数据类型转化器示例

    -
    [SocketDataConverter(Type = typeof(MockEntitySocketDataConverter))]
    +    
    [SocketDataConverter(Type = typeof(SocketDataConverter<MockEntity>))]
     class MockEntity
     {
         [SocketDataProperty(Type = typeof(byte[]), Offset = 0, Length = 5)]
    @@ -107,9 +106,8 @@ class MockEntity
     
         [SocketDataProperty(Type = typeof(Foo), Offset = 7, Length = 1, ConverterType = typeof(FooConverter), ConverterParameters = ["test"])]
         public string? Value1 { get; set; }
    -}
    -
    -class FooConverter(string name) : ISocketDataPropertyConverter
    +}
    +
    class FooConverter(string name) : ISocketDataPropertyConverter
     {
         public object? Convert(ReadOnlyMemory<byte> data)
         {
    diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json
    index 309e0b2fbd5..61a1c4dba1a 100644
    --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json
    +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json
    @@ -4839,7 +4839,7 @@
         "SocketComponents": "Socket 服务",
         "SocketAutoReceive": "自动接收数据",
         "SocketManualReceive": "手动接收数据",
    -    "DataPackageAdapter": "数据处理器",
    +    "DataPackageAdapter": "数据适配器",
         "SocketAutoConnect": "自动重连",
         "SocketDataEntity": "通讯数据转实体类",
         "NetworkMonitor": "网络状态 NetworkMonitor"
    
    From f4fe42b22f333a07e504ff27e190379563508f4b Mon Sep 17 00:00:00 2001
    From: Argo Zhang 
    Date: Sat, 19 Jul 2025 11:39:51 +0800
    Subject: [PATCH 4/4] =?UTF-8?q?test:=20=E6=8F=90=E9=AB=98=E4=BB=A3?=
     =?UTF-8?q?=E7=A0=81=E8=A6=86=E7=9B=96=E7=8E=87?=
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    ---
     test/UnitTest/Services/TcpSocketFactoryTest.cs | 12 ++++++++++++
     1 file changed, 12 insertions(+)
    
    diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs
    index e6b8e66124e..740d723558b 100644
    --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs
    +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs
    @@ -751,6 +751,10 @@ public async Task TryConvertTo_Ok()
             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);
         }
     
         private static TcpListener StartTcpServer(int port, Func handler)
    @@ -1188,6 +1192,14 @@ class MockEntity
             public string? Value13 { get; set; }
         }
     
    +    class MockSocketDataConverter: SocketDataConverter
    +    {
    +        protected override bool Parse(ReadOnlyMemory data, MockEntity entity)
    +        {
    +            return false;
    +        }
    +    }
    +
         class FooConverter(string name) : ISocketDataPropertyConverter
         {
             public object? Convert(ReadOnlyMemory data)