diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor index b3d4ea5509e..8bf08597bda 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor @@ -1,6 +1,14 @@ @page "/socket/adapter" @inject IStringLocalizer Localizer + + + +

@Localizer["AdaptersTitle"]

@Localizer["AdaptersDescription"]

@@ -31,8 +39,22 @@
  • 响应头为 4 字节定长,响应体为 8 个字节定长
  • 响应体为字符串类型数据
  • +

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

    +
    private readonly DataPackageAdapter _dataAdapter = new()
    +{
    +    // 数据适配器内部使用固定长度数据处理器
    +    DataPackageHandler = new FixLengthDataPackageHandler(12)
    +};
    +
    +_dataAdapter.ReceivedCallBack += async Data =>
    +{
    +    // 此处接收到的数据 Data 为完整响应数据
    +};
    +
    + +
    diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs index 488174594d2..0532684ddcd 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone -using BootstrapBlazor.Server.Components.Components; using System.Net; using System.Text; @@ -26,6 +25,11 @@ public partial class Adapters : IDisposable private readonly CancellationTokenSource _connectTokenSource = new(); private readonly CancellationTokenSource _sendTokenSource = new(); private readonly CancellationTokenSource _receiveTokenSource = new(); + private readonly DataPackageAdapter _dataAdapter = new() + { + DataPackageHandler = new FixLengthDataPackageHandler(12) + }; + private bool _useDataAdapter = true; /// /// @@ -38,10 +42,17 @@ protected override void OnInitialized() _client = TcpSocketFactory.GetOrCreate("demo-adapter", options => { // 关闭自动接收功能 - options.IsAutoReceive = false; + options.IsAutoReceive = true; // 设置本地使用的 IP地址与端口 options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); }); + _client.ReceivedCallBack += OnReceivedAsync; + + _dataAdapter.ReceivedCallBack += async Data => + { + // 直接处理接收的数据 + await UpdateReceiveLog(Data); + }; } private async Task OnConnectAsync() @@ -67,26 +78,49 @@ private async Task OnSendAsync() "2025"u8.CopyTo(data); Encoding.UTF8.GetBytes(DateTime.Now.ToString("ddHHmmss")).CopyTo(data, 4); var result = await _client.SendAsync(data, _sendTokenSource.Token); - if (result) + var state = result ? "成功" : "失败"; + + // 记录日志 + _items.Add(new ConsoleMessageItem() { - // 发送成功 - var payload = await _client.ReceiveAsync(_receiveTokenSource.Token); - if (!payload.IsEmpty) - { - // 解析接收到的数据 - // 响应头: 4 字节表示响应体长度 [0x32, 0x30, 0x32, 0x35] - // 响应体: 8 字节当前时间戳字符串 - data = payload.ToArray(); - var body = BitConverter.ToString(data); - _items.Add(new ConsoleMessageItem() - { - Message = $"{DateTime.Now}: 接收到来自 {_serverEndPoint} 数据: {Encoding.UTF8.GetString(data)} HEX: {body}" - }); - } - } + Message = $"{DateTime.Now}: 发送数据 {_client.LocalEndPoint} - {_serverEndPoint} Data {BitConverter.ToString(data)} {state}" + }); } } + private async ValueTask OnReceivedAsync(ReadOnlyMemory data) + { + if (_useDataAdapter) + { + // 使用数据适配器处理接收的数据 + await _dataAdapter.ReceiveAsync(data, _receiveTokenSource.Token); + } + else + { + // 直接处理接收的数据 + await UpdateReceiveLog(data); + } + } + + private async Task UpdateReceiveLog(ReadOnlyMemory data) + { + var payload = System.Text.Encoding.UTF8.GetString(data.Span); + var body = BitConverter.ToString(data.ToArray()); + + _items.Add(new ConsoleMessageItem + { + Message = $"{DateTime.Now}: 接收数据 {_client.LocalEndPoint} - {_serverEndPoint} Data {payload} HEX: {body}", + Color = Color.Success + }); + + // 保持队列中最大数量为 50 + if (_items.Count > 50) + { + _items.RemoveAt(0); + } + await InvokeAsync(StateHasChanged); + } + private async Task OnCloseAsync() { if (_client is { IsConnected: true }) @@ -111,6 +145,8 @@ private void Dispose(bool disposing) { if (disposing) { + _client.ReceivedCallBack -= OnReceivedAsync; + // 释放连接令牌资源 _connectTokenSource.Cancel(); _connectTokenSource.Dispose(); diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index cbfa88e0e44..af3aed1e7dd 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -7216,5 +7216,11 @@ "ReceivesDescription": "Receive data through Socket and display it", "NormalTitle": "Basic usage", "NormalIntro": "After connecting, the timestamp data sent by the server is automatically received through the ReceivedCallBack callback method" + }, + "BootstrapBlazor.Server.Components.Samples.Sockets.Adapters": { + "AdaptersTitle": "DataPackageAdapter", + "AdaptersDescription": "Receive data through the data adapter and display", + "NormalTitle": "Basic usage", + "NormalIntro": "After the connection is established, the timestamp data sent by the server is received through the ReceivedCallBack callback method of the DataPackageAdapter data adapter." } } diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index b012a2ed8c5..bf1d81163c7 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -7216,5 +7216,11 @@ "ReceivesDescription": "通过 Socket 接收数据并且显示", "NormalTitle": "基本用法", "NormalIntro": "连接后通过 ReceivedCallBack 回调方法自动接收服务端发送来的时间戳数据" + }, + "BootstrapBlazor.Server.Components.Samples.Sockets.Adapters": { + "AdaptersTitle": "Socket 数据适配器示例", + "AdaptersDescription": "通过数据适配器接收数据并且显示", + "NormalTitle": "基本用法", + "NormalIntro": "连接后通过 DataPackageAdapter 数据适配器的 ReceivedCallBack 回调方法接收服务端发送来的时间戳数据" } } diff --git a/src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs b/src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs index d9ed2acabd9..bdea30c1753 100644 --- a/src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs +++ b/src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Sockets; +using System.Text; namespace Longbow.Tasks.Services; @@ -55,11 +56,12 @@ private async Task OnDataHandlerAsync(TcpClient client, CancellationToken stoppi // 发送响应数据 // 响应头: 4 字节表示响应体长度 [0x32, 0x30, 0x32, 0x35] - // 响应体: 8 字节当前时间戳字符串 - var data = new byte[12]; - "2025"u8.ToArray().CopyTo(data, 0); - System.Text.Encoding.UTF8.GetBytes(DateTime.Now.ToString("ddHHmmss")).CopyTo(data, 4); - await stream.WriteAsync(data, stoppingToken); + // 响应体: 8 字节当前时间戳字符串 + // 此处模拟分包操作故意分 2 次写入数据,导致客户端接收 2 次才能得到完整数据 + await stream.WriteAsync("2025"u8.ToArray(), stoppingToken); + // 模拟延时 + await Task.Delay(40, stoppingToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes(DateTime.Now.ToString("ddHHmmss")), stoppingToken); } catch (OperationCanceledException) { break; } catch (IOException) { break; } diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageAdapter.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageAdapter.cs new file mode 100644 index 00000000000..d19f2736871 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageAdapter.cs @@ -0,0 +1,63 @@ +// 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 + +namespace BootstrapBlazor.Components; + +/// +/// 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 ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default) + { + if (DataPackageHandler != null) + { + if (DataPackageHandler.ReceivedCallBack == null) + { + DataPackageHandler.ReceivedCallBack = OnHandlerReceivedCallBack; + } + + // 如果存在数据处理器则调用其处理方法 + await DataPackageHandler.ReceiveAsync(data, token); + } + } + + /// + /// 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/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs index c669f845f11..0661a8295a8 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs @@ -48,7 +48,6 @@ public override async ValueTask ReceiveAsync(ReadOnlyMemory data, Cancella { await ReceivedCallBack(_data); } - continue; } } } diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageAdapter.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageAdapter.cs new file mode 100644 index 00000000000..f91aa15ecca --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageAdapter.cs @@ -0,0 +1,42 @@ +// 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 + +namespace BootstrapBlazor.Components; + +/// +/// 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 ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default); +} diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs index ea8da06cd76..705bb70f45e 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs @@ -31,13 +31,15 @@ public interface IDataPackageHandler ValueTask> SendAsync(ReadOnlyMemory data, CancellationToken token = default); /// - /// Asynchronously receives data from a source and writes it into the provided memory buffer. + /// Asynchronously receives data and processes it. /// - /// This method does not guarantee that the entire buffer will be filled. The number of bytes - /// written depends on the availability of data. - /// The memory buffer to store the received data. The buffer must be writable and have sufficient capacity. - /// A cancellation token that can be used to cancel the operation. The default value is . - /// A task that represents the asynchronous operation. The task result contains the number of bytes written to the - /// buffer. Returns 0 if the end of the data stream is reached. + /// 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 ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index 1d38e26dff3..c764661c93b 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -38,12 +38,6 @@ public interface ITcpSocketClient : IAsyncDisposable /// impact performance. Func, ValueTask>? ReceivedCallBack { get; set; } - /// - /// Configures the data handler to process incoming data packages. - /// - /// The handler responsible for processing data packages. Cannot be null. - void SetDataHandler(IDataPackageHandler handler); - /// /// Establishes an asynchronous connection to the specified endpoint. /// diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs index 5c6c0960246..7dacfc7e435 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs @@ -60,23 +60,10 @@ public abstract class TcpSocketClientBase(SocketClientOptions options) : ITcpSoc /// public Func, ValueTask>? ReceivedCallBack { get; set; } - /// - /// Gets or sets the handler responsible for processing data packages. - /// - public IDataPackageHandler? DataPackageHandler { get; protected set; } - private IPEndPoint? _remoteEndPoint; private IPEndPoint? _localEndPoint; private CancellationTokenSource? _receiveCancellationTokenSource; - /// - /// - /// - public virtual void SetDataHandler(IDataPackageHandler handler) - { - DataPackageHandler = handler; - } - /// /// /// @@ -157,12 +144,6 @@ public virtual async ValueTask SendAsync(ReadOnlyMemory data, Cancel var sendTokenSource = new CancellationTokenSource(options.SendTimeout); sendToken = CancellationTokenSource.CreateLinkedTokenSource(token, sendTokenSource.Token).Token; } - - if (DataPackageHandler != null) - { - data = await DataPackageHandler.SendAsync(data, sendToken); - } - ret = await SocketClientProvider.SendAsync(data, sendToken); } catch (OperationCanceledException ex) @@ -246,14 +227,9 @@ private async ValueTask ReceiveCoreAsync(ISocketClientProvider client, Memo if (ReceivedCallBack != null) { + // 如果订阅回调则触发回调 await ReceivedCallBack(buffer); } - - if (DataPackageHandler != null) - { - await DataPackageHandler.ReceiveAsync(buffer, receiveToken); - } - len = buffer.Length; } } catch (OperationCanceledException ex)