diff --git a/src/BootstrapBlazor.Server/Components/Pages/Introduction.razor b/src/BootstrapBlazor.Server/Components/Pages/Introduction.razor index f837ef9ca92..27956981a48 100644 --- a/src/BootstrapBlazor.Server/Components/Pages/Introduction.razor +++ b/src/BootstrapBlazor.Server/Components/Pages/Introduction.razor @@ -1,5 +1,6 @@ @page "/docs" @page "/introduction" +@page "/components"

@Localizer["Title"]

diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs index 0532684ddcd..4baa2bf2d1e 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs @@ -93,7 +93,7 @@ private async ValueTask OnReceivedAsync(ReadOnlyMemory data) if (_useDataAdapter) { // 使用数据适配器处理接收的数据 - await _dataAdapter.ReceiveAsync(data, _receiveTokenSource.Token); + await _dataAdapter.HandlerAsync(data, _receiveTokenSource.Token); } else { diff --git a/src/BootstrapBlazor/Extensions/ITcpSocketClientExtensions.cs b/src/BootstrapBlazor/Extensions/ITcpSocketClientExtensions.cs index 785afffb34a..cf58a2b31ad 100644 --- a/src/BootstrapBlazor/Extensions/ITcpSocketClientExtensions.cs +++ b/src/BootstrapBlazor/Extensions/ITcpSocketClientExtensions.cs @@ -48,4 +48,68 @@ public static ValueTask ConnectAsync(this ITcpSocketClient client, string var endPoint = Utility.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 custom data package adapter and a callback + /// function for processing received data. + /// + /// This method sets up the to use the provided for handling incoming data. The adapter processes the raw data received by the client and + /// attempts to convert it into an instance of . If the conversion is successful, the + /// is invoked with the converted entity; otherwise, it is invoked with . + /// The type of the entity that the data package adapter will attempt to convert the received data into. + /// The instance to configure. + /// The instance responsible for handling and processing incoming data. + /// A callback function to be invoked with the processed data of type . The callback + /// receives if the data cannot be converted to . + public static void SetDataPackageAdapter(this ITcpSocketClient client, IDataPackageAdapter adapter, Func callback) + { + // 设置 ITcpSocketClient 的回调函数 + client.ReceivedCallBack = async buffer => + { + // 将接收到的数据传递给 DataPackageAdapter 进行数据处理合规数据触发 ReceivedCallBack 回调 + await adapter.HandlerAsync(buffer); + }; + + // 设置 DataPackageAdapter 的回调函数 + adapter.ReceivedCallBack = async buffer => + { + TEntity? ret = default; + if (adapter.TryConvertTo(buffer, out var t)) + { + if (t is TEntity entity) + { + ret = entity; + } + } + await callback(ret); + }; + } } diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageAdapter.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageAdapter.cs index d19f2736871..a1b7e98e940 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageAdapter.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageAdapter.cs @@ -30,7 +30,7 @@ public class DataPackageAdapter : IDataPackageAdapter /// /// /// - public virtual async ValueTask ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default) + public virtual async ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default) { if (DataPackageHandler != null) { @@ -40,10 +40,22 @@ public virtual async ValueTask ReceiveAsync(ReadOnlyMemory data, Cancellat } // 如果存在数据处理器则调用其处理方法 - await DataPackageHandler.ReceiveAsync(data, token); + await DataPackageHandler.HandlerAsync(data, token); } } + /// + /// + /// + /// + /// + /// + public virtual bool TryConvertTo(ReadOnlyMemory data, [NotNullWhen(true)] out object? entity) + { + entity = null; + return false; + } + /// /// Handles incoming data by invoking a callback method, if one is defined. /// diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs index 89a760fa93a..41677e72a7b 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs @@ -26,7 +26,7 @@ public abstract class DataPackageHandlerBase : IDataPackageHandler /// /// /// - public abstract ValueTask ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default); + public abstract ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default); /// /// Handles the processing of a sticky package by adjusting the provided buffer and length. diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs index 1eee4fdf3ed..e8923643006 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs @@ -52,7 +52,7 @@ public DelimiterDataPackageHandler(byte[] delimiter) /// /// /// - public override async ValueTask ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default) + public override async ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default) { data = ConcatBuffer(data); diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs index 0661a8295a8..40289ace4ff 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs @@ -24,7 +24,7 @@ public class FixLengthDataPackageHandler(int length) : DataPackageHandlerBase /// /// /// - public override async ValueTask ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default) + public override async ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default) { while (data.Length > 0) { diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageAdapter.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageAdapter.cs index f91aa15ecca..f603c528460 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageAdapter.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageAdapter.cs @@ -38,5 +38,16 @@ public interface IDataPackageAdapter /// not provided. /// A representing the asynchronous operation. The task completes when the data has been /// successfully received and processed. - ValueTask ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default); + ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default); + + /// + /// Attempts to convert the specified binary data into an object representation. + /// + /// This method does not throw an exception if the conversion fails. Instead, it returns and sets to . + /// The binary data to be converted. Must not be empty. + /// When this method returns , contains the converted object. When this method returns , contains . + /// if the conversion was successful; otherwise, . + bool TryConvertTo(ReadOnlyMemory data, [NotNullWhen(true)] out object? entity); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs index 74da06af81b..25dbd85f445 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs @@ -29,5 +29,5 @@ public interface IDataPackageHandler /// provided. /// A containing if the data was successfully received and /// processed; otherwise, . - ValueTask ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default); + ValueTask HandlerAsync(ReadOnlyMemory data, CancellationToken token = default); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs index 5329eec3549..8c5134c8528 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs @@ -148,9 +148,14 @@ public virtual async ValueTask SendAsync(ReadOnlyMemory data, Cancel } catch (OperationCanceledException ex) { - Log(LogLevel.Warning, ex, token.IsCancellationRequested - ? $"TCP Socket send operation was canceled from {_localEndPoint} to {_remoteEndPoint}" - : $"TCP Socket send operation timed out from {_localEndPoint} to {_remoteEndPoint}"); + if (token.IsCancellationRequested) + { + 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) { diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index d38e1d4544a..1f7ccb317ae 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -308,22 +308,16 @@ public async Task FixLengthDataPackageHandler_Ok() // 设置数据适配器 var adapter = new DataPackageAdapter { - DataPackageHandler = new FixLengthDataPackageHandler(7), - ReceivedCallBack = buffer => - { - // buffer 即是接收到的数据 - buffer.CopyTo(receivedBuffer); - receivedBuffer = receivedBuffer[..buffer.Length]; - tcs.SetResult(); - return ValueTask.CompletedTask; - } + DataPackageHandler = new FixLengthDataPackageHandler(7) }; - - client.ReceivedCallBack = async buffer => + client.SetDataPackageAdapter(adapter, buffer => { - // 将接收到的数据传递给 DataPackageAdapter - await adapter.ReceiveAsync(buffer); - }; + // buffer 即是接收到的数据 + buffer.CopyTo(receivedBuffer); + receivedBuffer = receivedBuffer[..buffer.Length]; + tcs.SetResult(); + return ValueTask.CompletedTask; + }); // 测试 ConnectAsync 方法 var connect = await client.ConnectAsync("localhost", port); @@ -350,7 +344,7 @@ public async Task FixLengthDataPackageHandler_Sticky() var server = StartTcpServer(port, MockStickyPackageAsync); var client = CreateClient(); var tcs = new TaskCompletionSource(); - var receivedBuffer = new byte[1024]; + var receivedBuffer = new byte[128]; // 连接 TCP Server var connect = await client.ConnectAsync("localhost", port); @@ -358,22 +352,17 @@ public async Task FixLengthDataPackageHandler_Sticky() // 设置数据适配器 var adapter = new DataPackageAdapter { - DataPackageHandler = new FixLengthDataPackageHandler(7), - ReceivedCallBack = buffer => - { - // buffer 即是接收到的数据 - buffer.CopyTo(receivedBuffer); - receivedBuffer = receivedBuffer[..buffer.Length]; - tcs.SetResult(); - return ValueTask.CompletedTask; - } + DataPackageHandler = new FixLengthDataPackageHandler(7) }; - client.ReceivedCallBack = async buffer => + client.SetDataPackageAdapter(adapter, buffer => { - // 将接收到的数据传递给 DataPackageAdapter - await adapter.ReceiveAsync(buffer); - }; + // buffer 即是接收到的数据 + buffer.CopyTo(receivedBuffer); + receivedBuffer = receivedBuffer[..buffer.Length]; + tcs.SetResult(); + return ValueTask.CompletedTask; + }); // 发送数据 var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); @@ -408,31 +397,25 @@ public async Task FixLengthDataPackageHandler_Sticky() [Fact] public async Task DelimiterDataPackageHandler_Ok() { - var port = 8886; + var port = 8883; var server = StartTcpServer(port, MockDelimiterPackageAsync); var client = CreateClient(); var tcs = new TaskCompletionSource(); - var receivedBuffer = new byte[1024]; + var receivedBuffer = new byte[128]; // 设置数据适配器 var adapter = new DataPackageAdapter { - DataPackageHandler = new DelimiterDataPackageHandler(new byte[] { 13, 10 }), - ReceivedCallBack = buffer => - { - // buffer 即是接收到的数据 - buffer.CopyTo(receivedBuffer); - receivedBuffer = receivedBuffer[..buffer.Length]; - tcs.SetResult(); - return ValueTask.CompletedTask; - } + DataPackageHandler = new DelimiterDataPackageHandler([13, 10]), }; - - client.ReceivedCallBack = async buffer => + client.SetDataPackageAdapter(adapter, buffer => { - // 将接收到的数据传递给 DataPackageAdapter - await adapter.ReceiveAsync(buffer); - }; + // buffer 即是接收到的数据 + buffer.CopyTo(receivedBuffer); + receivedBuffer = receivedBuffer[..buffer.Length]; + tcs.SetResult(); + return ValueTask.CompletedTask; + }); // 连接 TCP Server var connect = await client.ConnectAsync("localhost", port); @@ -467,6 +450,78 @@ public async Task DelimiterDataPackageHandler_Ok() Assert.NotNull(ex); } + [Fact] + public async Task TryConvertTo_Ok() + { + var port = 8886; + var server = StartTcpServer(port, MockSplitPackageAsync); + var client = CreateClient(); + var tcs = new TaskCompletionSource(); + MockEntity? entity = null; + + // 设置数据适配器 + var adapter = new MockEntityDataPackageAdapter + { + DataPackageHandler = new FixLengthDataPackageHandler(7), + }; + client.SetDataPackageAdapter(adapter, 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(entity.Header, [1, 2, 3, 4, 5]); + Assert.Equal(entity.Body, [3, 4]); + + // 测试异常流程 + var adapter2 = new DataPackageAdapter(); + var result = adapter2.TryConvertTo(data, out var t); + Assert.False(result); + Assert.Null(t); + } + + [Fact] + public async Task TryConvertTo_Null() + { + var port = 8890; + var server = StartTcpServer(port, MockSplitPackageAsync); + var client = CreateClient(); + var tcs = new TaskCompletionSource(); + MockEntity? entity = null; + + // 设置数据适配器 + var adapter = new MockErrorEntityDataPackageAdapter + { + DataPackageHandler = new FixLengthDataPackageHandler(7), + }; + client.SetDataPackageAdapter(adapter, 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.Null(entity); + } + private static TcpListener StartTcpServer(int port, Func handler) { var server = new TcpListener(IPAddress.Loopback, port); @@ -574,10 +629,8 @@ private static ITcpSocketClient CreateClient(Action? builder builder.AddProvider(new MockLoggerProvider()); }); sc.AddBootstrapBlazorTcpSocketFactory(); - if (builder != null) - { - builder(sc); - } + builder?.Invoke(sc); + var provider = sc.BuildServiceProvider(); var factory = provider.GetRequiredService(); var client = factory.GetOrCreate("test", op => op.LocalEndPoint = Utility.ConvertToIpEndPoint("localhost", 0)); @@ -619,7 +672,7 @@ class MockSendErrorSocketProvider : ISocketClientProvider { public bool IsConnected { get; private set; } - public IPEndPoint LocalEndPoint { get; set; } + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); public ValueTask CloseAsync() { @@ -647,7 +700,7 @@ class MockSendTimeoutSocketProvider : ISocketClientProvider { public bool IsConnected { get; private set; } - public IPEndPoint LocalEndPoint { get; set; } + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); public ValueTask CloseAsync() { @@ -672,4 +725,33 @@ public async ValueTask SendAsync(ReadOnlyMemory data, CancellationTo return false; } } + + class MockEntityDataPackageAdapter : DataPackageAdapter + { + public override bool TryConvertTo(ReadOnlyMemory data, [NotNullWhen(true)] out object? entity) + { + entity = new MockEntity + { + Header = data[..5].ToArray(), + Body = data[5..].ToArray() + }; + return true; + } + } + + class MockErrorEntityDataPackageAdapter : DataPackageAdapter + { + public override bool TryConvertTo(ReadOnlyMemory data, [NotNullWhen(true)] out object? entity) + { + entity = new Foo(); + return true; + } + } + + class MockEntity + { + public byte[]? Header { get; set; } + + public byte[]? Body { get; set; } + } }