Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

// 连接远端节点 连接成功后自动开始接收数据
Expand All @@ -78,8 +77,36 @@ private async Task CreateClient()
<li><code>DelimiterDataPackageHandler</code> <b>分隔符数据处理器</b> 即通讯包以特定一个或一组字节分割</li>
</ul>

<p class="code-label">5. 数据适配器(设计中)</p>
<p class="code-label">5. 数据适配器</p>

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

<p>数据适配器设计思路如下</p>

<ol class="ul-demo">
<li>使用 <code>SocketDataConverterAttribute</code> 标签约定通讯数据使用那个转换类型进行转换 指定类型需继承 <code>ISocketDataConverter</code>
接口
</li>
<li>使用 <code>SocketDataPropertyAttribute</code> 标签约定如何转换数据类型 (Property) 属性值</li>
</ol>

<Pre>[SocketDataConverter(Type = typeof(SocketDataConverter&lt;MockEntity&gt;))]
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; }
}</Pre>

<Pre>class FooConverter(string name) : ISocketDataPropertyConverter
{
public object? Convert(ReadOnlyMemory&lt;byte&gt; data)
{
return new Foo() { Id = data.Span[0], Name = name };
}
}</Pre>
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
</ul>
<p>本示例服务器端模拟了数据分包即响应数据实际是两次写入所以实际接收端是要通过两次接收才能得到一个完整的响应数据包,可通过 <b>数据适配器</b> 来简化接收逻辑。通过切换下方 <b>是否使用数据适配器</b> 控制开关进行测试查看实际数据接收情况。</p>
<ul class="ul-demo">
<li>不使用 <b>数据处理器</b> 要分两次接收才能接收完整</li>
<li>使用 <b>数据处理器</b> 一次即可接收完整数据包</li>
<li>不使用 <b>数据适配器</b> 要分两次接收才能接收完整</li>
<li>使用 <b>数据适配器</b> 一次即可接收完整数据包</li>
</ul>
<Pre>private readonly DataPackageAdapter _dataAdapter = new()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}

Expand Down Expand Up @@ -104,7 +104,7 @@ private async ValueTask OnReceivedAsync(ReadOnlyMemory<byte> data)

private async Task UpdateReceiveLog(ReadOnlyMemory<byte> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Name="Normal" ShowCode="false">
<p>通过 <code>SocketDataConverterAttribute</code> 类标签与 <code>SocketDataPropertyAttribute</code>
属性标签可以将通讯数据自动转化为我们系统中需要的业务实体类,示例如下:</p>
<Pre>[SocketDataConverter(Type = typeof(MockEntitySocketDataConverter))]
<Pre>[SocketDataConverter(Type = typeof(SocketDataConverter&lt;MockEntity&gt;))]
class MockEntity
{
[SocketDataProperty(Type = typeof(byte[]), Offset = 0, Length = 5)]
Expand All @@ -26,8 +26,7 @@ class MockEntity
}</Pre>
<p class="code-label">1. <code>SocketDataConverter</code> 参数说明:</p>
<ul class="ul-demo">
<li><b>Type</b>: 自定义转换器类型,组件库内置了 <code>SocketDataConverterBase</code> 泛型基类,继承后非常方便扩展出自己的转换器,重载
<code>TryConvertTo</code> 方法调用基类的 <code>Parse</code> 基本就完成了 99% 的工作量,也可以完全自己解析通讯电文到实体类的各个属性上
<li><b>Type</b>: 自定义转换器类型,组件库内置了 <code>SocketDataConverter</code> 泛型类,建议看一下源码非常方便扩展出自己的转换器
</li>
</ul>
<Pre>class MockEntitySocketDataConverter : SocketDataConverterBase&lt;MockEntity&gt;
Expand Down Expand Up @@ -96,7 +95,7 @@ class MockEntity
<li><code>SocketDataUInt64LittleEndianConverter</code> 转成 ulong 无符号整形小端读取</li>
</ul>
<p>自定义数据类型转化器示例</p>
<Pre>[SocketDataConverter(Type = typeof(MockEntitySocketDataConverter))]
<Pre>[SocketDataConverter(Type = typeof(SocketDataConverter&lt;MockEntity&gt;))]
class MockEntity
{
[SocketDataProperty(Type = typeof(byte[]), Offset = 0, Length = 5)]
Expand All @@ -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
}</Pre>
<Pre>class FooConverter(string name) : ISocketDataPropertyConverter
{
public object? Convert(ReadOnlyMemory&lt;byte&gt; data)
{
Expand Down
2 changes: 1 addition & 1 deletion src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -4839,7 +4839,7 @@
"SocketComponents": "Socket 服务",
"SocketAutoReceive": "自动接收数据",
"SocketManualReceive": "手动接收数据",
"DataPackageAdapter": "数据处理器",
"DataPackageAdapter": "数据适配器",
"SocketAutoConnect": "自动重连",
"SocketDataEntity": "通讯数据转实体类",
"NetworkMonitor": "网络状态 NetworkMonitor"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,27 @@ namespace BootstrapBlazor.Components;
/// Provides a base class for converting socket data into a specified entity type.
/// </summary>
/// <typeparam name="TEntity">The type of entity to convert the socket data into.</typeparam>
public abstract class SocketDataConverterBase<TEntity> : ISocketDataConverter<TEntity>
public class SocketDataConverter<TEntity> : ISocketDataConverter<TEntity>
{
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="data"></param>
/// <param name="entity"></param>
/// <returns></returns>
public abstract bool TryConvertTo(ReadOnlyMemory<byte> data, [NotNullWhen(true)] out TEntity? entity);
public virtual bool TryConvertTo(ReadOnlyMemory<byte> data, [NotNullWhen(true)] out TEntity? entity)
{
var v = CreateEntity();
var ret = Parse(data, v);
entity = ret ? v : default;
return ret;
}

/// <summary>
/// 创建实体实例方法
/// </summary>
/// <returns></returns>
protected virtual TEntity CreateEntity() => Activator.CreateInstance<TEntity>();

/// <summary>
/// 将字节数据转换为指定实体类型的实例。
Expand Down
8 changes: 4 additions & 4 deletions src/BootstrapBlazor/Services/TcpSocket/SocketClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// Represents configuration options for a socket client, including buffer sizes, timeouts, and endpoints.
/// </summary>
/// <remarks>Use this class to configure various settings for a socket client, such as connection timeouts,
/// <remarks>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.</remarks>
public class SocketClientOptions
Expand Down Expand Up @@ -49,14 +49,14 @@ public class SocketClientOptions
/// Gets or sets the local endpoint for the socket client. Default value is <see cref="IPAddress.Any"/>
/// </summary>
/// <remarks>This property specifies the local network endpoint that the socket client will bind to when establishing a connection.</remarks>
public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0);
public IPEndPoint LocalEndPoint { get; set; } = new(IPAddress.Any, 0);

/// <summary>
/// Gets or sets a value indicating whether logging is enabled. Default value is false.
/// </summary>
public bool EnableLog { get; set; }
/// <summary>

/// <summary>
/// Gets or sets a value indicating whether the system should automatically attempt to reconnect after a connection is lost. Default value is false.
/// </summary>
public bool IsAutoReconnect { get; set; }
Expand Down
29 changes: 15 additions & 14 deletions test/UnitTest/Services/TcpSocketFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ public async Task TryConvertTo_Ok()
{
DataPackageHandler = new FixLengthDataPackageHandler(29),
};
client.SetDataPackageAdapter(adapter, new MockEntitySocketDataConverter(), t =>
client.SetDataPackageAdapter(adapter, new SocketDataConverter<MockEntity>(), t =>
{
entity = t;
tcs.SetResult();
Expand Down Expand Up @@ -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<MockEntity>(), out var t);
Assert.True(result);
Assert.NotNull(t);
Assert.Equal([1, 2, 3, 4, 5], entity.Header);
Expand All @@ -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<TcpClient, Task> handler)
Expand Down Expand Up @@ -1137,18 +1141,7 @@ public void SetReceive(bool state)
}
}

class MockEntitySocketDataConverter : SocketDataConverterBase<MockEntity>
{
public override bool TryConvertTo(ReadOnlyMemory<byte> 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<MockEntity>))]
class MockEntity
{
[SocketDataProperty(Type = typeof(byte[]), Offset = 0, Length = 5)]
Expand Down Expand Up @@ -1199,6 +1192,14 @@ class MockEntity
public string? Value13 { get; set; }
}

class MockSocketDataConverter: SocketDataConverter<MockEntity>
{
protected override bool Parse(ReadOnlyMemory<byte> data, MockEntity entity)
{
return false;
}
}

class FooConverter(string name) : ISocketDataPropertyConverter
{
public object? Convert(ReadOnlyMemory<byte> data)
Expand Down
Loading