From c7f2dff85ba5aaf9c74a9ef5d8702587632fe17b Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 15 Jun 2025 17:39:47 +0800 Subject: [PATCH 01/37] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E6=96=B9=E6=B3=95=E5=88=A4=E6=96=AD=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E6=98=AF=E5=90=A6=E4=B8=BA=20IsWasm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/HostEnvironmentExtensions.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/BootstrapBlazor/Extensions/HostEnvironmentExtensions.cs diff --git a/src/BootstrapBlazor/Extensions/HostEnvironmentExtensions.cs b/src/BootstrapBlazor/Extensions/HostEnvironmentExtensions.cs new file mode 100644 index 00000000000..15819a28492 --- /dev/null +++ b/src/BootstrapBlazor/Extensions/HostEnvironmentExtensions.cs @@ -0,0 +1,21 @@ +// 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.Hosting; + +namespace BootstrapBlazor.Components; + +/// +/// 扩展方法" +/// +public static class HostEnvironmentExtensions +{ + /// + /// 当前程序是否为 WebAssembly 环境 + /// + /// + /// + public static bool IsWasm(this IHostEnvironment hostEnvironment) => hostEnvironment is MockWasmHostEnvironment; +} From c777b6f1024151a2c50fccc11474eafddeed7bb0 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 16 Jun 2025 10:38:27 +0800 Subject: [PATCH 02/37] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20ITcpSocketFa?= =?UTF-8?q?ctory=20=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 69 +++++++++++++++++++ .../TcpSocket/DefaultTcpSocketFactory.cs | 43 ++++++++++++ .../Services/TcpSocket/ITcpSocketClient.cs | 50 ++++++++++++++ .../Services/TcpSocket/ITcpSocketFactory.cs | 23 +++++++ .../Services/TcpSocket/SocketMode.cs | 22 ++++++ .../Services/TcpSocket/TcpSocketExtensions.cs | 30 ++++++++ 6 files changed, 237 insertions(+) create mode 100644 src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs create mode 100644 src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs create mode 100644 src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs create mode 100644 src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs create mode 100644 src/BootstrapBlazor/Services/TcpSocket/SocketMode.cs create mode 100644 src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs new file mode 100644 index 00000000000..2f6cd5ee6e2 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -0,0 +1,69 @@ +// 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 System.Net.Sockets; +using System.Runtime.Versioning; + +namespace BootstrapBlazor.Components; + +[UnsupportedOSPlatform("browser")] +class DefaultTcpSocketClient(string host, int port) : ITcpSocketClient +{ + private TcpClient? _client; + + public bool IsConnected => _client?.Connected ?? false; + + public ILogger? Logger { get; set; } + + public async Task ConnectAsync(CancellationToken token = default) + { + var ret = false; + + try + { + _client ??= new TcpClient(host, port); + await _client.ConnectAsync(host, port, token); + ret = true; + } + catch (Exception ex) + { + Logger?.LogError(ex, "TCP Socket connection failed to {Host}:{Port}", host, port); + } + return ret; + } + + private void Dispose(bool disposing) + { + if (disposing) + { + // 释放托管资源 + if (_client != null) + { + try + { + _client.Close(); + } + catch (Exception ex) + { + Logger?.LogError(ex, "Error closing TCP Socket connection to {Host}:{Port}", host, port); + } + finally + { + _client = null; + } + } + } + } + + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs new file mode 100644 index 00000000000..58781c312f6 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs @@ -0,0 +1,43 @@ +// 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 System.Collections.Concurrent; +using System.Runtime.Versioning; + +namespace BootstrapBlazor.Components; + +[UnsupportedOSPlatform("browser")] +class DefaultTcpSocketFactory(Logger logger) : ITcpSocketFactory +{ + private readonly ConcurrentDictionary _pool = new(); + + public ITcpSocketClient GetOrCreate(string host, int port, SocketMode mode = SocketMode.Client) + { + return _pool.GetOrAdd($"{host}:{port}", key => new DefaultTcpSocketClient(host, port) { Logger = logger }); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + // 释放托管资源 + foreach (var socket in _pool.Values) + { + socket.Dispose(); + } + _pool.Clear(); + } + } + + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs new file mode 100644 index 00000000000..aa5ac641892 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -0,0 +1,50 @@ +// 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; + +/// +/// Represents a TCP socket for network communication. +/// +public interface ITcpSocketClient : IDisposable +{ + /// + /// Gets a value indicating whether the system is currently connected. + /// + bool IsConnected { get; } + + /// + /// Asynchronously establishes a connection to the server. + /// + /// This method attempts to connect to the server and returns a value indicating whether the + /// connection was successful. If the connection cannot be established within the timeout period specified by the + /// , the operation is canceled. + /// A that can be used to cancel the connection attempt. If not provided, the + /// default token is used. + /// if the connection is successfully established; otherwise, . + Task ConnectAsync(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, . + Task SendAsync(byte[] data, CancellationToken token = default); + + /// + /// Asynchronously receives data from the underlying connection. + /// + /// The method waits for data to be available and returns it as a byte array. If the operation is + /// canceled via the , the returned task will be in a canceled state. + /// A that can be used to cancel the operation. The default value is . + /// A task that represents the asynchronous operation. The task result contains a byte array with the received data. + Task ReceiveAsync(CancellationToken token = default); +} diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs new file mode 100644 index 00000000000..efa67e26276 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs @@ -0,0 +1,23 @@ +// 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; + +/// +/// ITcpSocketFactory Interface +/// +public interface ITcpSocketFactory : IDisposable +{ + /// + /// Retrieves an existing TCP socket associated with the specified host and port, or creates a new one if none + /// exists. + /// + /// The hostname or IP address of the remote endpoint. Cannot be null or empty. + /// The port number of the remote endpoint. Must be a valid port number between 0 and 65535. + /// The mode of the socket, specifying whether it operates as a client or server. Defaults to . + /// An instance representing the TCP socket for the specified host and port. + ITcpSocketClient GetOrCreate(string host, int port, SocketMode mode = SocketMode.Client); +} diff --git a/src/BootstrapBlazor/Services/TcpSocket/SocketMode.cs b/src/BootstrapBlazor/Services/TcpSocket/SocketMode.cs new file mode 100644 index 00000000000..fdf2057abe1 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/SocketMode.cs @@ -0,0 +1,22 @@ +// 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; + +/// +/// Socket 工作模式 +/// +public enum SocketMode +{ + /// + /// Client 模式 + /// + Client, + + /// + /// Server 模式 + /// + Server +} diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs new file mode 100644 index 00000000000..b2de8974156 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs @@ -0,0 +1,30 @@ +// 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.DependencyInjection.Extensions; +using System.Runtime.Versioning; + +namespace BootstrapBlazor.Components; + +/// +/// TcpSocket 扩展方法 +/// +[UnsupportedOSPlatform("browser")] +public static class TcpSocketExtensions +{ + /// + /// 增加 + /// + /// + /// + public static IServiceCollection AddTcpSocketClient(this IServiceCollection services) + { + // 添加 ITcpSocket 实现 + services.TryAddSingleton(); + + return services; + } +} From 130949e5f541d31c62aac8fd26966d47619bea43 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 16 Jun 2025 17:42:27 +0800 Subject: [PATCH 03/37] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=20ConnectA?= =?UTF-8?q?sync=20=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 65 ++++++++++++++++++- .../TcpSocket/DefaultTcpSocketFactory.cs | 4 +- .../Services/TcpSocket/ITcpSocketClient.cs | 31 ++++----- .../Services/TcpSocket/TcpSocketExtensions.cs | 2 +- 4 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index 2f6cd5ee6e2..aaf0ec1f976 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -4,6 +4,7 @@ // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using Microsoft.Extensions.Logging; +using System.Buffers; using System.Net.Sockets; using System.Runtime.Versioning; @@ -18,13 +19,12 @@ class DefaultTcpSocketClient(string host, int port) : ITcpSocketClient public ILogger? Logger { get; set; } - public async Task ConnectAsync(CancellationToken token = default) + public async Task ConnectAsync(string host, int port, CancellationToken token = default) { var ret = false; - try { - _client ??= new TcpClient(host, port); + _client ??= new TcpClient(); await _client.ConnectAsync(host, port, token); ret = true; } @@ -58,6 +58,65 @@ private void Dispose(bool disposing) } } + public async Task SendAsync(Memory data, CancellationToken token = default) + { + if (_client is not { Connected: true }) + { + throw new InvalidOperationException("TCP Socket is not connected."); + } + + var ret = false; + try + { + var stream = _client.GetStream(); + await stream.WriteAsync(data, token); + ret = true; + } + catch (OperationCanceledException ex) + { + Logger?.LogWarning(ex, "TCP Socket send operation was canceled to {Host}:{Port}", host, port); + } + catch (Exception ex) + { + Logger?.LogError(ex, "TCP Socket send failed to {Host}:{Port}", host, port); + } + return ret; + } + + public async Task> ReceiveAsync(int bufferSize = 1024 * 10, CancellationToken token = default) + { + if (_client is not { Connected: true }) + { + throw new InvalidOperationException("TCP Socket is not connected."); + } + + var block = ArrayPool.Shared.Rent(bufferSize); + var buffer = new Memory(block); + try + { + var stream = _client.GetStream(); + var len = await stream.ReadAsync(buffer, token); + if (len == 0) + { + Logger?.LogInformation("TCP Socket received {len} data from {Host}:{Port}", len, host, port); + } + else + { + buffer = buffer[..len]; + } + } + catch (OperationCanceledException ex) + { + Logger?.LogWarning(ex, "TCP Socket receive operation was canceled to {Host}:{Port}", host, port); + } + catch (Exception ex) + { + Logger?.LogError(ex, "TCP Socket receive failed to {Host}:{Port}", host, port); + } + ArrayPool.Shared.Return(block); + return buffer; + } + /// /// /// diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs index 58781c312f6..0603742b222 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs @@ -10,13 +10,13 @@ namespace BootstrapBlazor.Components; [UnsupportedOSPlatform("browser")] -class DefaultTcpSocketFactory(Logger logger) : ITcpSocketFactory +class DefaultTcpSocketFactory() : ITcpSocketFactory { private readonly ConcurrentDictionary _pool = new(); public ITcpSocketClient GetOrCreate(string host, int port, SocketMode mode = SocketMode.Client) { - return _pool.GetOrAdd($"{host}:{port}", key => new DefaultTcpSocketClient(host, port) { Logger = logger }); + return _pool.GetOrAdd($"{host}:{port}", key => new DefaultTcpSocketClient(host, port) { }); } private void Dispose(bool disposing) diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index aa5ac641892..bad0963a901 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -16,15 +16,15 @@ public interface ITcpSocketClient : IDisposable bool IsConnected { get; } /// - /// Asynchronously establishes a connection to the server. + /// Establishes an asynchronous connection to the specified host and port. /// - /// This method attempts to connect to the server and returns a value indicating whether the - /// connection was successful. If the connection cannot be established within the timeout period specified by the - /// , the operation is canceled. - /// A that can be used to cancel the connection attempt. If not provided, the - /// default token is used. - /// if the connection is successfully established; otherwise, . - Task ConnectAsync(CancellationToken token = default); + /// 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, . + Task ConnectAsync(string host, int port, CancellationToken token = default); /// /// Sends the specified data asynchronously to the target endpoint. @@ -36,15 +36,16 @@ public interface ITcpSocketClient : IDisposable /// 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, . - Task SendAsync(byte[] data, CancellationToken token = default); + Task SendAsync(Memory data, CancellationToken token = default); /// - /// Asynchronously receives data from the underlying connection. + /// Asynchronously receives data into a memory buffer of the specified size. /// - /// The method waits for data to be available and returns it as a byte array. If the operation is - /// canceled via the , the returned task will be in a canceled state. - /// A that can be used to cancel the operation. The default value is The size of the buffer, in bytes, to allocate for receiving data. Must be greater than zero. Defaults to 10,240 + /// bytes. + /// A to observe while waiting for the operation to complete. Defaults to . - /// A task that represents the asynchronous operation. The task result contains a byte array with the received data. - Task ReceiveAsync(CancellationToken token = default); + /// A of type containing the received data. The memory may be empty if no + /// data is received. + Task> ReceiveAsync(int bufferSize = 1024 * 10, CancellationToken token = default); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs index b2de8974156..81fde060e97 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs @@ -20,7 +20,7 @@ public static class TcpSocketExtensions /// /// /// - public static IServiceCollection AddTcpSocketClient(this IServiceCollection services) + public static IServiceCollection AddBootstrapBlazorTcpSocketFactory(this IServiceCollection services) { // 添加 ITcpSocket 实现 services.TryAddSingleton(); From c23703d8a2185743273f5bd17fdcd739fc9a8f50 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 16 Jun 2025 17:42:36 +0800 Subject: [PATCH 04/37] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitTest/Services/TcpSocketFactoryTest.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 test/UnitTest/Services/TcpSocketFactoryTest.cs diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs new file mode 100644 index 00000000000..4a6fa66ab41 --- /dev/null +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -0,0 +1,80 @@ +// 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 System.Net; +using System.Net.Sockets; + +namespace UnitTest.Services; + +public class TcpSocketFactoryTest +{ + [Fact] + public async Task TcpSocketFactory_Ok() + { + var server = StartTcpServer(); + + var sc = new ServiceCollection(); + sc.AddBootstrapBlazorTcpSocketFactory(); + + var provider = sc.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + var client = factory.GetOrCreate("localhost", 0); + + // 测试 ConnectAsync 方法 + var connect = await client.ConnectAsync("localhost", 8888); + Assert.True(connect); + Assert.True(client.IsConnected); + + // 测试 SendAsync 方法 + var data = new Memory([1, 2, 3, 4, 5]); + var result = await client.SendAsync(data); + Assert.True(result); + + // 测试 ReceiveAsync 方法 + var buffer = await client.ReceiveAsync(); + Assert.Equal(buffer.ToArray(), [1, 2, 3, 4, 5]); + StopTcpServer(server); + } + + private static TcpListener StartTcpServer() + { + var server = new TcpListener(IPAddress.Loopback, 8888); + server.Start(); + Task.Run(AcceptClientsAsync); + return server; + + async Task AcceptClientsAsync() + { + while (true) + { + var client = await server.AcceptTcpClientAsync(); + _ = Task.Run(() => HandleClientAsync(client)); + } + } + + async Task HandleClientAsync(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 Memory(buffer, 0, len); + await stream.WriteAsync(block, CancellationToken.None); + } + } + } + + private static void StopTcpServer(TcpListener server) + { + server?.Stop(); + } +} From c0d1ac5db62123074bdf6369fec318ead76c399c Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 16 Jun 2025 18:45:14 +0800 Subject: [PATCH 05/37] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=20ITcpSock?= =?UTF-8?q?etClient=20=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 77 +++++++++++-------- .../TcpSocket/DefaultTcpSocketFactory.cs | 10 ++- .../Services/TcpSocket/ITcpSocketClient.cs | 14 ++++ .../Services/TcpSocket/TcpSocketExtensions.cs | 2 + 4 files changed, 66 insertions(+), 37 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index aaf0ec1f976..41113f754e9 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -5,59 +5,45 @@ using Microsoft.Extensions.Logging; using System.Buffers; +using System.Net; using System.Net.Sockets; using System.Runtime.Versioning; namespace BootstrapBlazor.Components; [UnsupportedOSPlatform("browser")] -class DefaultTcpSocketClient(string host, int port) : ITcpSocketClient +class DefaultTcpSocketClient(ILogger logger) : ITcpSocketClient { private TcpClient? _client; public bool IsConnected => _client?.Connected ?? false; - public ILogger? Logger { get; set; } + public Task ConnectAsync(string host, int port, CancellationToken token = default) + { + if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + host = IPAddress.Loopback.ToString(); + } + var endPoint = new IPEndPoint(IPAddress.Parse(host), port); + return ConnectAsync(endPoint, token); + } - public async Task ConnectAsync(string host, int port, CancellationToken token = default) + public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) { var ret = false; try { _client ??= new TcpClient(); - await _client.ConnectAsync(host, port, token); + await _client.ConnectAsync(endPoint, token); ret = true; } catch (Exception ex) { - Logger?.LogError(ex, "TCP Socket connection failed to {Host}:{Port}", host, port); + logger.LogError(ex, "TCP Socket connection failed to {EndPoint}", endPoint); } return ret; } - private void Dispose(bool disposing) - { - if (disposing) - { - // 释放托管资源 - if (_client != null) - { - try - { - _client.Close(); - } - catch (Exception ex) - { - Logger?.LogError(ex, "Error closing TCP Socket connection to {Host}:{Port}", host, port); - } - finally - { - _client = null; - } - } - } - } - public async Task SendAsync(Memory data, CancellationToken token = default) { if (_client is not { Connected: true }) @@ -74,11 +60,11 @@ public async Task SendAsync(Memory data, CancellationToken token = d } catch (OperationCanceledException ex) { - Logger?.LogWarning(ex, "TCP Socket send operation was canceled to {Host}:{Port}", host, port); + logger.LogWarning(ex, "TCP Socket send operation was canceled to {EndPoint}", _client.Client.RemoteEndPoint); } catch (Exception ex) { - Logger?.LogError(ex, "TCP Socket send failed to {Host}:{Port}", host, port); + logger.LogError(ex, "TCP Socket send failed to {EndPoint}", _client.Client.RemoteEndPoint); } return ret; } @@ -98,7 +84,7 @@ public async Task> ReceiveAsync(int bufferSize = 1024 * 10, Cancell var len = await stream.ReadAsync(buffer, token); if (len == 0) { - Logger?.LogInformation("TCP Socket received {len} data from {Host}:{Port}", len, host, port); + logger.LogInformation("TCP Socket received {len} data from {EndPoint}", len, _client.Client.RemoteEndPoint); } else { @@ -107,16 +93,39 @@ public async Task> ReceiveAsync(int bufferSize = 1024 * 10, Cancell } catch (OperationCanceledException ex) { - Logger?.LogWarning(ex, "TCP Socket receive operation was canceled to {Host}:{Port}", host, port); + logger.LogWarning(ex, "TCP Socket receive operation was canceled to {EndPoint}", _client.Client.RemoteEndPoint); } catch (Exception ex) { - Logger?.LogError(ex, "TCP Socket receive failed to {Host}:{Port}", host, port); + logger.LogError(ex, "TCP Socket receive failed to {EndPoint}", _client.Client.RemoteEndPoint); + } + finally + { + ArrayPool.Shared.Return(block); } - ArrayPool.Shared.Return(block); return buffer; } + private void Dispose(bool disposing) + { + if (disposing) + { + // 释放托管资源 + if (_client != null) + { + try + { + _client.Close(); + } + catch (Exception) { } + finally + { + _client = null; + } + } + } + } + /// /// /// diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs index 0603742b222..5133ac555b3 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs @@ -3,20 +3,24 @@ // 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.DependencyInjection; using System.Collections.Concurrent; using System.Runtime.Versioning; namespace BootstrapBlazor.Components; [UnsupportedOSPlatform("browser")] -class DefaultTcpSocketFactory() : ITcpSocketFactory +class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory { private readonly ConcurrentDictionary _pool = new(); public ITcpSocketClient GetOrCreate(string host, int port, SocketMode mode = SocketMode.Client) { - return _pool.GetOrAdd($"{host}:{port}", key => new DefaultTcpSocketClient(host, port) { }); + return _pool.GetOrAdd($"{host}:{port}", key => + { + var client = provider.GetRequiredService(); + return client; + }); } private void Dispose(bool disposing) diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index bad0963a901..5894de03fd4 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone +using System.Net; + namespace BootstrapBlazor.Components; /// @@ -26,6 +28,18 @@ public interface ITcpSocketClient : IDisposable /// is successfully established; otherwise, . Task ConnectAsync(string host, int port, CancellationToken token = default); + /// + /// 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, . + Task ConnectAsync(IPEndPoint endPoint, CancellationToken token = default); + /// /// Sends the specified data asynchronously to the target endpoint. /// diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs index 81fde060e97..b29d8e88e86 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs @@ -25,6 +25,8 @@ public static IServiceCollection AddBootstrapBlazorTcpSocketFactory(this IServic // 添加 ITcpSocket 实现 services.TryAddSingleton(); + // 添加 ITcpSocketClient 实现 + services.TryAddTransient(); return services; } } From 2ce01c1ca4723b33fa434f6f63dc4994d5e19b97 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 16 Jun 2025 18:45:20 +0800 Subject: [PATCH 06/37] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitTest/Services/TcpSocketFactoryTest.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index 4a6fa66ab41..6f098b0f08c 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -3,6 +3,7 @@ // 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 System.Net; using System.Net.Sockets; @@ -16,6 +17,10 @@ public async Task TcpSocketFactory_Ok() var server = StartTcpServer(); var sc = new ServiceCollection(); + sc.AddLogging(builder => + { + builder.AddProvider(new MockLoggerProvider()); + }); sc.AddBootstrapBlazorTcpSocketFactory(); var provider = sc.BuildServiceProvider(); @@ -77,4 +82,35 @@ private static void StopTcpServer(TcpListener server) { server?.Stop(); } + + 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) + { + + } + } } From f5f0ba12a8c75759ffdcf0d10f944a7f2cfc1391 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 11:12:05 +0800 Subject: [PATCH 07/37] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=AE=9E=E4=BE=8B=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 48 ++++++++++++++----- .../TcpSocket/DefaultTcpSocketFactory.cs | 8 +++- .../Services/TcpSocket/ITcpSocketClient.cs | 8 ++++ .../Services/TcpSocket/TcpSocketExtensions.cs | 2 - 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index 41113f754e9..d7dfaa58fba 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -12,19 +12,28 @@ namespace BootstrapBlazor.Components; [UnsupportedOSPlatform("browser")] -class DefaultTcpSocketClient(ILogger logger) : ITcpSocketClient +class DefaultTcpSocketClient : ITcpSocketClient { private TcpClient? _client; public bool IsConnected => _client?.Connected ?? false; + public IPEndPoint LocalEndPoint { get; } + + public ILogger? Logger { get; set; } + + public DefaultTcpSocketClient(string host, int port = 0) + { + LocalEndPoint = new IPEndPoint(GetIPAddress(host), port); + } + + private static IPAddress GetIPAddress(string host) => host.Equals("localhost", StringComparison.OrdinalIgnoreCase) + ? IPAddress.Loopback + : IPAddress.TryParse(host, out var ip) ? ip : Dns.GetHostAddresses(host).FirstOrDefault() ?? IPAddress.Loopback; + public Task ConnectAsync(string host, int port, CancellationToken token = default) { - if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) - { - host = IPAddress.Loopback.ToString(); - } - var endPoint = new IPEndPoint(IPAddress.Parse(host), port); + var endPoint = new IPEndPoint(GetIPAddress(host), port); return ConnectAsync(endPoint, token); } @@ -39,7 +48,7 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke } catch (Exception ex) { - logger.LogError(ex, "TCP Socket connection failed to {EndPoint}", endPoint); + LogError(ex, $"TCP Socket connection failed to {endPoint}"); } return ret; } @@ -60,11 +69,11 @@ public async Task SendAsync(Memory data, CancellationToken token = d } catch (OperationCanceledException ex) { - logger.LogWarning(ex, "TCP Socket send operation was canceled to {EndPoint}", _client.Client.RemoteEndPoint); + LogWarning(ex, $"TCP Socket send operation was canceled to {_client.Client.RemoteEndPoint}"); } catch (Exception ex) { - logger.LogError(ex, "TCP Socket send failed to {EndPoint}", _client.Client.RemoteEndPoint); + LogError(ex, $"TCP Socket send failed to {_client.Client.RemoteEndPoint}"); } return ret; } @@ -84,7 +93,7 @@ public async Task> ReceiveAsync(int bufferSize = 1024 * 10, Cancell var len = await stream.ReadAsync(buffer, token); if (len == 0) { - logger.LogInformation("TCP Socket received {len} data from {EndPoint}", len, _client.Client.RemoteEndPoint); + LogInformation($"TCP Socket received {len} data from {_client.Client.RemoteEndPoint}"); } else { @@ -93,11 +102,11 @@ public async Task> ReceiveAsync(int bufferSize = 1024 * 10, Cancell } catch (OperationCanceledException ex) { - logger.LogWarning(ex, "TCP Socket receive operation was canceled to {EndPoint}", _client.Client.RemoteEndPoint); + LogWarning(ex, $"TCP Socket receive operation was canceled to {_client.Client.RemoteEndPoint}"); } catch (Exception ex) { - logger.LogError(ex, "TCP Socket receive failed to {EndPoint}", _client.Client.RemoteEndPoint); + LogError(ex, $"TCP Socket receive failed to {_client.Client.RemoteEndPoint}"); } finally { @@ -106,6 +115,21 @@ public async Task> ReceiveAsync(int bufferSize = 1024 * 10, Cancell return buffer; } + private void LogInformation(string message) + { + Logger?.LogInformation("{message}", message); + } + + private void LogWarning(Exception ex, string message) + { + Logger?.LogWarning(ex, "{message}", message); + } + + private void LogError(Exception ex, string message) + { + Logger?.LogError(ex, "{message}", message); + } + private void Dispose(bool disposing) { if (disposing) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs index 5133ac555b3..1ef20409826 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs @@ -4,6 +4,7 @@ // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Runtime.Versioning; @@ -14,11 +15,14 @@ class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory { private readonly ConcurrentDictionary _pool = new(); - public ITcpSocketClient GetOrCreate(string host, int port, SocketMode mode = SocketMode.Client) + public ITcpSocketClient GetOrCreate(string host, int port = 0, SocketMode mode = SocketMode.Client) { return _pool.GetOrAdd($"{host}:{port}", key => { - var client = provider.GetRequiredService(); + var client = new DefaultTcpSocketClient(host, port) + { + Logger = provider.GetService>() + }; return client; }); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index 5894de03fd4..01722d79fae 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -17,6 +17,14 @@ public interface ITcpSocketClient : IDisposable /// bool IsConnected { 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; } + /// /// Establishes an asynchronous connection to the specified host and port. /// diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs index b29d8e88e86..81fde060e97 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs @@ -25,8 +25,6 @@ public static IServiceCollection AddBootstrapBlazorTcpSocketFactory(this IServic // 添加 ITcpSocket 实现 services.TryAddSingleton(); - // 添加 ITcpSocketClient 实现 - services.TryAddTransient(); return services; } } From 95ccd158bf0af6a9cdf8112a3e0c559040d48ab1 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 11:17:06 +0800 Subject: [PATCH 08/37] =?UTF-8?q?refactor:=20=E7=B2=BE=E7=AE=80=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index d7dfaa58fba..589699930b9 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -134,18 +134,10 @@ private void Dispose(bool disposing) { if (disposing) { - // 释放托管资源 if (_client != null) { - try - { - _client.Close(); - } - catch (Exception) { } - finally - { - _client = null; - } + _client.Close(); + _client = null; } } } From 8898e7a73fa76cc21e242d258c86a9d7f3f84447 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 11:17:18 +0800 Subject: [PATCH 09/37] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E8=AE=B0=E5=BD=95=E6=97=A5=E5=BF=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index 589699930b9..e0ddec52dde 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -46,6 +46,10 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke await _client.ConnectAsync(endPoint, token); ret = true; } + catch (OperationCanceledException ex) + { + LogWarning(ex, $"TCP Socket connect operation was canceled to {endPoint}"); + } catch (Exception ex) { LogError(ex, $"TCP Socket connection failed to {endPoint}"); From 455b445870b3a647148a75fc8199e4f3f45a1508 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 11:35:16 +0800 Subject: [PATCH 10/37] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=20Close=20?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 9 +++++++++ .../Services/TcpSocket/ITcpSocketClient.cs | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index e0ddec52dde..46bb2759ffd 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -42,6 +42,10 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke var ret = false; try { + // 释放资源 + Close(); + + // 创建新的 TcpClient 实例 _client ??= new TcpClient(); await _client.ConnectAsync(endPoint, token); ret = true; @@ -119,6 +123,11 @@ public async Task> ReceiveAsync(int bufferSize = 1024 * 10, Cancell return buffer; } + public void Close() + { + Dispose(true); + } + private void LogInformation(string message) { Logger?.LogInformation("{message}", message); diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index 01722d79fae..a0c55f8ba80 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -70,4 +70,12 @@ public interface ITcpSocketClient : IDisposable /// A of type containing the received data. The memory may be empty if no /// data is received. Task> ReceiveAsync(int bufferSize = 1024 * 10, 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. + void Close(); } From 283caaa2874fca343b0e491eff87e0b184326475 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 11:35:29 +0800 Subject: [PATCH 11/37] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitTest/Services/TcpSocketFactoryTest.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index 6f098b0f08c..1144f52e25d 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -43,6 +43,26 @@ public async Task TcpSocketFactory_Ok() StopTcpServer(server); } + [Fact] + public void 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("localhost", 0); + client1.Close(); + + var client2 = factory.GetOrCreate("localhost", 0); + Assert.Equal(client1, client2); + } + private static TcpListener StartTcpServer() { var server = new TcpListener(IPAddress.Loopback, 8888); From 033b0c2d34625e3eded4c9bea8cd9e1ef379bf48 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 13:44:33 +0800 Subject: [PATCH 12/37] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20IDataPackage?= =?UTF-8?q?Adapter=20=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 7 ++++++ .../Services/TcpSocket/IDataPackageAdapter.cs | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/BootstrapBlazor/Services/TcpSocket/IDataPackageAdapter.cs diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index 46bb2759ffd..9d6219e14a9 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -22,6 +22,8 @@ class DefaultTcpSocketClient : ITcpSocketClient public ILogger? Logger { get; set; } + public IDataPackageAdapter? DataPackageAdapter { get; set; } + public DefaultTcpSocketClient(string host, int port = 0) { LocalEndPoint = new IPEndPoint(GetIPAddress(host), port); @@ -106,6 +108,11 @@ public async Task> ReceiveAsync(int bufferSize = 1024 * 10, Cancell else { buffer = buffer[..len]; + + if (DataPackageAdapter != null) + { + buffer = await DataPackageAdapter.ReceiveAsync(buffer); + } } } catch (OperationCanceledException ex) diff --git a/src/BootstrapBlazor/Services/TcpSocket/IDataPackageAdapter.cs b/src/BootstrapBlazor/Services/TcpSocket/IDataPackageAdapter.cs new file mode 100644 index 00000000000..d882a5bc304 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/IDataPackageAdapter.cs @@ -0,0 +1,25 @@ +// 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 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 IDataPackageAdapter +{ + /// + /// Asynchronously receives data and writes it into the specified memory buffer. + /// + /// The method does not guarantee that the entire buffer will be filled. The amount of data + /// written depends on the data available to be received. + /// The memory buffer where the received data will be written. The buffer must be large enough to hold the incoming + /// data. + /// A task that represents the asynchronous receive operation. + Task> ReceiveAsync(Memory memory); +} From 60c122e676c7f748729fc16c36c0bc9c0a430224 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 14:02:06 +0800 Subject: [PATCH 13/37] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E6=9C=AC=E5=9C=B0=E8=8A=82=E7=82=B9=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index 9d6219e14a9..ebd068fc336 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -48,7 +48,7 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke Close(); // 创建新的 TcpClient 实例 - _client ??= new TcpClient(); + _client ??= new TcpClient(LocalEndPoint); await _client.ConnectAsync(endPoint, token); ret = true; } From 86217e173f39e305dece0ae448d24deab5676b01 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 15:30:37 +0800 Subject: [PATCH 14/37] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=A4=84=E7=90=86=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DataPackageHandlerBase.cs | 35 +++++++++++++++++++ .../TcpSocket/DefaultTcpSocketClient.cs | 13 ++++++- ...ckageAdapter.cs => IDataPackageHandler.cs} | 17 +++++++-- .../Services/TcpSocket/ITcpSocketClient.cs | 9 +++++ 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs rename src/BootstrapBlazor/Services/TcpSocket/{IDataPackageAdapter.cs => IDataPackageHandler.cs} (52%) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs new file mode 100644 index 00000000000..44dfb9f5655 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs @@ -0,0 +1,35 @@ +// 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 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 +{ + /// + /// + /// + /// + /// + public Task> ReceiveAsync(Memory data) + { + return Task.FromResult(data); + } + + /// + /// + /// + /// + /// + public Task> SendAsync(Memory data) + { + return Task.FromResult(data); + } +} diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index ebd068fc336..c2f3d772324 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -15,6 +15,7 @@ namespace BootstrapBlazor.Components; class DefaultTcpSocketClient : ITcpSocketClient { private TcpClient? _client; + private List _dataPackageHandlers = []; public bool IsConnected => _client?.Connected ?? false; @@ -22,7 +23,7 @@ class DefaultTcpSocketClient : ITcpSocketClient public ILogger? Logger { get; set; } - public IDataPackageAdapter? DataPackageAdapter { get; set; } + public IDataPackageHandler? DataPackageAdapter { get; set; } public DefaultTcpSocketClient(string host, int port = 0) { @@ -33,6 +34,12 @@ private static IPAddress GetIPAddress(string host) => host.Equals("localhost", S ? IPAddress.Loopback : IPAddress.TryParse(host, out var ip) ? ip : Dns.GetHostAddresses(host).FirstOrDefault() ?? IPAddress.Loopback; + public void SetDataHandlers(params List handlers) + { + _dataPackageHandlers.Clear(); + _dataPackageHandlers.AddRange(handlers); + } + public Task ConnectAsync(string host, int port, CancellationToken token = default) { var endPoint = new IPEndPoint(GetIPAddress(host), port); @@ -73,6 +80,10 @@ public async Task SendAsync(Memory data, CancellationToken token = d var ret = false; try { + foreach (var handler in _dataPackageHandlers) + { + data = await handler.SendAsync(data); + } var stream = _client.GetStream(); await stream.WriteAsync(data, token); ret = true; diff --git a/src/BootstrapBlazor/Services/TcpSocket/IDataPackageAdapter.cs b/src/BootstrapBlazor/Services/TcpSocket/IDataPackageHandler.cs similarity index 52% rename from src/BootstrapBlazor/Services/TcpSocket/IDataPackageAdapter.cs rename to src/BootstrapBlazor/Services/TcpSocket/IDataPackageHandler.cs index d882a5bc304..b46b8024b3d 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/IDataPackageAdapter.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/IDataPackageHandler.cs @@ -11,15 +11,26 @@ namespace BootstrapBlazor.Components; /// 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 IDataPackageAdapter +public interface IDataPackageHandler { + /// + /// Sends the specified data asynchronously to the target destination. + /// + /// The method performs an asynchronous operation to send the provided data. The caller must + /// ensure that the data is valid and non-empty. The returned memory block may contain a response or acknowledgment + /// depending on the implementation of the target destination. + /// The data to be sent, represented as a block of memory. + /// A task that represents the asynchronous operation. The task result contains a of representing the response or acknowledgment received from the target destination. + Task> SendAsync(Memory data); + /// /// Asynchronously receives data and writes it into the specified memory buffer. /// /// The method does not guarantee that the entire buffer will be filled. The amount of data /// written depends on the data available to be received. - /// The memory buffer where the received data will be written. The buffer must be large enough to hold the incoming + /// The memory buffer where the received data will be written. The buffer must be large enough to hold the incoming /// data. /// A task that represents the asynchronous receive operation. - Task> ReceiveAsync(Memory memory); + Task> ReceiveAsync(Memory data); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index a0c55f8ba80..4e13972391c 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -25,6 +25,15 @@ public interface ITcpSocketClient : IDisposable /// specific local endpoint, this property may return . IPEndPoint LocalEndPoint { get; } + /// + /// Sets the collection of data package handlers to be used for processing data. + /// + /// The provided handlers must implement the interface. Ensure + /// that the list contains valid and properly configured handlers to avoid runtime issues. + /// A list of handlers that implement the interface. Each handler will be used to + /// process data packages in the order they appear in the list. + void SetDataHandlers(params List handlers); + /// /// Establishes an asynchronous connection to the specified host and port. /// From 415c5886ffd57e352958fb9ad6a66927f37e5427 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 15:39:33 +0800 Subject: [PATCH 15/37] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=20virtual?= =?UTF-8?q?=20=E5=85=B3=E9=94=AE=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DataPackageHandlerBase.cs | 4 ++-- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs index 44dfb9f5655..2e08cac8316 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs @@ -18,7 +18,7 @@ public abstract class DataPackageHandlerBase : IDataPackageHandler /// /// /// - public Task> ReceiveAsync(Memory data) + public virtual Task> ReceiveAsync(Memory data) { return Task.FromResult(data); } @@ -28,7 +28,7 @@ public Task> ReceiveAsync(Memory data) /// /// /// - public Task> SendAsync(Memory data) + public virtual Task> SendAsync(Memory data) { return Task.FromResult(data); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index c2f3d772324..a0813b5581b 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -23,8 +23,6 @@ class DefaultTcpSocketClient : ITcpSocketClient public ILogger? Logger { get; set; } - public IDataPackageHandler? DataPackageAdapter { get; set; } - public DefaultTcpSocketClient(string host, int port = 0) { LocalEndPoint = new IPEndPoint(GetIPAddress(host), port); @@ -120,9 +118,9 @@ public async Task> ReceiveAsync(int bufferSize = 1024 * 10, Cancell { buffer = buffer[..len]; - if (DataPackageAdapter != null) + foreach (var handler in _dataPackageHandlers) { - buffer = await DataPackageAdapter.ReceiveAsync(buffer); + buffer = await handler.ReceiveAsync(buffer); } } } From 2ef702da5412b68ef44ca8b2f66e2321e716c474 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 15:39:40 +0800 Subject: [PATCH 16/37] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Services/TcpSocketFactoryTest.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index 1144f52e25d..c0c37f7fbf4 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -37,9 +37,12 @@ public async Task TcpSocketFactory_Ok() var result = await client.SendAsync(data); Assert.True(result); + // 增加数据库处理适配器 + client.SetDataHandlers([new MockDataHandler()]); + // 测试 ReceiveAsync 方法 var buffer = await client.ReceiveAsync(); - Assert.Equal(buffer.ToArray(), [1, 2, 3, 4, 5]); + Assert.Equal(buffer.ToArray(), [1, 2, 3, 4, 5, 1, 2]); StopTcpServer(server); } @@ -103,6 +106,18 @@ private static void StopTcpServer(TcpListener server) server?.Stop(); } + class MockDataHandler : DataPackageHandlerBase + { + public override Task> ReceiveAsync(Memory data) + { + var buffer = new byte[data.Length + 2]; + data.CopyTo(buffer); + buffer[^2] = 0x01; + buffer[^1] = 0x02; + return Task.FromResult(new Memory(buffer)); + } + } + class MockLoggerProvider : ILoggerProvider { public ILogger CreateLogger(string categoryName) From 3162a0111263e4f424781b92d44a7eab251b3983 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 16:13:23 +0800 Subject: [PATCH 17/37] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Services/TcpSocketFactoryTest.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index c0c37f7fbf4..f0a3400bfd1 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -4,6 +4,7 @@ // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using Microsoft.Extensions.Logging; +using System.Buffers; using System.Net; using System.Net.Sockets; @@ -110,11 +111,11 @@ class MockDataHandler : DataPackageHandlerBase { public override Task> ReceiveAsync(Memory data) { - var buffer = new byte[data.Length + 2]; - data.CopyTo(buffer); - buffer[^2] = 0x01; - buffer[^1] = 0x02; - return Task.FromResult(new Memory(buffer)); + using var buffer = MemoryPool.Shared.Rent(data.Length + 2); + data.CopyTo(buffer.Memory); + buffer.Memory.Span[data.Length] = 0x01; + buffer.Memory.Span[data.Length + 1] = 0x02; + return Task.FromResult(buffer.Memory[..(data.Length + 2)]); } } From fda1c328a68ca561d51b4734a56fd459787cf150 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 20:49:04 +0800 Subject: [PATCH 18/37] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=A4=84=E7=90=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataPackage/DataPackageHandlerBase.cs | 47 ++++++++++++++++ .../FixLengthDataPackageHandler.cs | 54 +++++++++++++++++++ .../{ => DataPackage}/IDataPackageHandler.cs | 7 ++- .../TcpSocket/DataPackageHandlerBase.cs | 35 ------------ 4 files changed, 107 insertions(+), 36 deletions(-) create mode 100644 src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs create mode 100644 src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs rename src/BootstrapBlazor/Services/TcpSocket/{ => DataPackage}/IDataPackageHandler.cs (89%) delete mode 100644 src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs new file mode 100644 index 00000000000..6555217615c --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs @@ -0,0 +1,47 @@ +// 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 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 +{ + /// + /// 当接收数据处理完成后,回调该函数执行接收 + /// + public Func, Task>? ReceivedCallBack { get; set; } + + /// + /// Sends the specified data asynchronously to the target destination. + /// + /// The method performs an asynchronous operation to send the provided data. The caller must + /// ensure that the data is valid and non-empty. The returned memory block may contain a response or acknowledgment + /// depending on the implementation of the target destination. + /// The data to be sent, represented as a block of memory. + /// A task that represents the asynchronous operation. The task result contains a of representing the response or acknowledgment received from the target destination. + public virtual Task> SendAsync(Memory data) + { + return Task.FromResult(data); + } + + /// + /// Asynchronously receives data and writes it into the specified memory buffer. + /// + /// The method does not guarantee that the entire buffer will be filled. The amount of data + /// written depends on the data available to be received. + /// The memory buffer where the received data will be written. The buffer must be large enough to hold the incoming + /// data. + /// A task that represents the asynchronous receive operation. + public virtual Task ReceiveAsync(Memory data) + { + return Task.FromResult(data); + } +} diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs new file mode 100644 index 00000000000..4812eb02ce2 --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs @@ -0,0 +1,54 @@ +// 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 System.Drawing; + +namespace BootstrapBlazor.Components; + +/// +/// 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 Task ReceiveAsync(Memory data) + { + if (_receivedLength == 0) + { + data.CopyTo(_data); + _receivedLength = data.Length; + return; + } + + // 拷贝数据 + var len = length - _receivedLength; + var segment = data.Length > len ? data[..len] : data; + segment.CopyTo(_data[_receivedLength..]); + + // 更新已接收长度 + _receivedLength += segment.Length; + + // 如果已接收长度等于总长度则触发回调 + if (_receivedLength == length) + { + if (ReceivedCallBack != null) + { + await ReceivedCallBack(_data); + } + } + } +} diff --git a/src/BootstrapBlazor/Services/TcpSocket/IDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs similarity index 89% rename from src/BootstrapBlazor/Services/TcpSocket/IDataPackageHandler.cs rename to src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs index b46b8024b3d..edc2904c44a 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/IDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs @@ -13,6 +13,11 @@ namespace BootstrapBlazor.Components; /// tailored to specific application protocols. public interface IDataPackageHandler { + /// + /// Gets or sets the callback function to be invoked when data is received asynchronously. + /// + Func, Task>? ReceivedCallBack { get; set; } + /// /// Sends the specified data asynchronously to the target destination. /// @@ -32,5 +37,5 @@ public interface IDataPackageHandler /// The memory buffer where the received data will be written. The buffer must be large enough to hold the incoming /// data. /// A task that represents the asynchronous receive operation. - Task> ReceiveAsync(Memory data); + Task ReceiveAsync(Memory data); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs deleted file mode 100644 index 2e08cac8316..00000000000 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackageHandlerBase.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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 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 -{ - /// - /// - /// - /// - /// - public virtual Task> ReceiveAsync(Memory data) - { - return Task.FromResult(data); - } - - /// - /// - /// - /// - /// - public virtual Task> SendAsync(Memory data) - { - return Task.FromResult(data); - } -} From 2d087f68cb10e682e75e42c5cf29b63924d2c775 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 20:49:21 +0800 Subject: [PATCH 19/37] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=90=8E=E8=87=AA=E5=8A=A8=E6=8E=A5=E6=94=B6=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 81 ++++++++++--------- .../{ => Extensions}/TcpSocketExtensions.cs | 0 .../Services/TcpSocket/ITcpSocketClient.cs | 20 +---- 3 files changed, 47 insertions(+), 54 deletions(-) rename src/BootstrapBlazor/Services/TcpSocket/{ => Extensions}/TcpSocketExtensions.cs (100%) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index a0813b5581b..b15f800799d 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -15,7 +15,9 @@ namespace BootstrapBlazor.Components; class DefaultTcpSocketClient : ITcpSocketClient { private TcpClient? _client; - private List _dataPackageHandlers = []; + private IDataPackageHandler? _dataPackageHandler; + private Task? _receiveTask; + private CancellationTokenSource? _receiveCancellationTokenSource; public bool IsConnected => _client?.Connected ?? false; @@ -23,6 +25,8 @@ class DefaultTcpSocketClient : ITcpSocketClient public ILogger? Logger { get; set; } + public int ReceiveBufferSize { get; set; } = 1024 * 10; + public DefaultTcpSocketClient(string host, int port = 0) { LocalEndPoint = new IPEndPoint(GetIPAddress(host), port); @@ -32,10 +36,9 @@ private static IPAddress GetIPAddress(string host) => host.Equals("localhost", S ? IPAddress.Loopback : IPAddress.TryParse(host, out var ip) ? ip : Dns.GetHostAddresses(host).FirstOrDefault() ?? IPAddress.Loopback; - public void SetDataHandlers(params List handlers) + public void SetDataHandler(IDataPackageHandler handler) { - _dataPackageHandlers.Clear(); - _dataPackageHandlers.AddRange(handlers); + _dataPackageHandler = handler; } public Task ConnectAsync(string host, int port, CancellationToken token = default) @@ -55,6 +58,9 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke // 创建新的 TcpClient 实例 _client ??= new TcpClient(LocalEndPoint); await _client.ConnectAsync(endPoint, token); + + // 开始接收数据 + _ = Task.Run(ReceiveAsync); ret = true; } catch (OperationCanceledException ex) @@ -78,9 +84,9 @@ public async Task SendAsync(Memory data, CancellationToken token = d var ret = false; try { - foreach (var handler in _dataPackageHandlers) + if (_dataPackageHandler != null) { - data = await handler.SendAsync(data); + data = await _dataPackageHandler.SendAsync(data); } var stream = _client.GetStream(); await stream.WriteAsync(data, token); @@ -97,46 +103,47 @@ public async Task SendAsync(Memory data, CancellationToken token = d return ret; } - public async Task> ReceiveAsync(int bufferSize = 1024 * 10, CancellationToken token = default) + private async Task ReceiveAsync() { - if (_client is not { Connected: true }) + using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); + var buffer = block.Memory; + _receiveCancellationTokenSource ??= new(); + while (_receiveCancellationTokenSource is { IsCancellationRequested: false }) { - throw new InvalidOperationException("TCP Socket is not connected."); - } - - var block = ArrayPool.Shared.Rent(bufferSize); - var buffer = new Memory(block); - try - { - var stream = _client.GetStream(); - var len = await stream.ReadAsync(buffer, token); - if (len == 0) + if (_client is not { Connected: true }) { - LogInformation($"TCP Socket received {len} data from {_client.Client.RemoteEndPoint}"); + throw new InvalidOperationException("TCP Socket is not connected."); } - else - { - buffer = buffer[..len]; - foreach (var handler in _dataPackageHandlers) + try + { + var stream = _client.GetStream(); + var len = await stream.ReadAsync(buffer, _receiveCancellationTokenSource.Token); + if (len == 0) + { + // 远端主机关闭链路 + LogInformation($"TCP Socket received {len} data from {_client.Client.RemoteEndPoint}"); + break; + } + else { - buffer = await handler.ReceiveAsync(buffer); + buffer = buffer[..len]; + + if (_dataPackageHandler != null) + { + await _dataPackageHandler.ReceiveAsync(buffer); + } } } + catch (OperationCanceledException ex) + { + LogWarning(ex, $"TCP Socket receive operation was canceled to {_client.Client.RemoteEndPoint}"); + } + catch (Exception ex) + { + LogError(ex, $"TCP Socket receive failed to {_client.Client.RemoteEndPoint}"); + } } - catch (OperationCanceledException ex) - { - LogWarning(ex, $"TCP Socket receive operation was canceled to {_client.Client.RemoteEndPoint}"); - } - catch (Exception ex) - { - LogError(ex, $"TCP Socket receive failed to {_client.Client.RemoteEndPoint}"); - } - finally - { - ArrayPool.Shared.Return(block); - } - return buffer; } public void Close() diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs b/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs similarity index 100% rename from src/BootstrapBlazor/Services/TcpSocket/TcpSocketExtensions.cs rename to src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index 4e13972391c..cac839f5bb4 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -26,13 +26,10 @@ public interface ITcpSocketClient : IDisposable IPEndPoint LocalEndPoint { get; } /// - /// Sets the collection of data package handlers to be used for processing data. + /// Configures the data handler to process incoming data packages. /// - /// The provided handlers must implement the interface. Ensure - /// that the list contains valid and properly configured handlers to avoid runtime issues. - /// A list of handlers that implement the interface. Each handler will be used to - /// process data packages in the order they appear in the list. - void SetDataHandlers(params List handlers); + /// The handler responsible for processing data packages. Cannot be null. + void SetDataHandler(IDataPackageHandler handler); /// /// Establishes an asynchronous connection to the specified host and port. @@ -69,17 +66,6 @@ public interface ITcpSocketClient : IDisposable /// sent successfully; otherwise, . Task SendAsync(Memory data, CancellationToken token = default); - /// - /// Asynchronously receives data into a memory buffer of the specified size. - /// - /// The size of the buffer, in bytes, to allocate for receiving data. Must be greater than zero. Defaults to 10,240 - /// bytes. - /// A to observe while waiting for the operation to complete. Defaults to . - /// A of type containing the received data. The memory may be empty if no - /// data is received. - Task> ReceiveAsync(int bufferSize = 1024 * 10, CancellationToken token = default); - /// /// Closes the current connection or resource, releasing any associated resources. /// From 0474f6650110c06a8adb9c921d9f03e10be1c7ea Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 20:49:28 +0800 Subject: [PATCH 20/37] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitTest/Services/TcpSocketFactoryTest.cs | 104 ++++++++++++++++-- 1 file changed, 96 insertions(+), 8 deletions(-) diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index f0a3400bfd1..cc232c57ec9 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -33,17 +33,20 @@ public async Task TcpSocketFactory_Ok() Assert.True(connect); Assert.True(client.IsConnected); + // 增加数据库处理适配器 + client.SetDataHandler(new MockDataHandler() + { + //ReceivedCallBack = buffer => + //{ + //} + }); + // 测试 SendAsync 方法 var data = new Memory([1, 2, 3, 4, 5]); var result = await client.SendAsync(data); Assert.True(result); - // 增加数据库处理适配器 - client.SetDataHandlers([new MockDataHandler()]); - - // 测试 ReceiveAsync 方法 - var buffer = await client.ReceiveAsync(); - Assert.Equal(buffer.ToArray(), [1, 2, 3, 4, 5, 1, 2]); + //Assert.Equal(buffer.ToArray(), [1, 2, 3, 4, 5, 1, 2]); StopTcpServer(server); } @@ -67,9 +70,88 @@ public void GetOrCreate_Ok() Assert.Equal(client1, client2); } - private static TcpListener StartTcpServer() + [Fact] + public async Task FixLengthDataPackageHandler_Ok() { - var server = new TcpListener(IPAddress.Loopback, 8888); + var server = StartDataPackageTcpServer(); + + var sc = new ServiceCollection(); + sc.AddLogging(builder => + { + builder.AddProvider(new MockLoggerProvider()); + }); + sc.AddBootstrapBlazorTcpSocketFactory(); + + var provider = sc.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + var client = factory.GetOrCreate("localhost", 0); + + // 测试 ConnectAsync 方法 + var connect = await client.ConnectAsync("localhost", 8899); + Assert.True(connect); + Assert.True(client.IsConnected); + + var tcs = new TaskCompletionSource(); + Memory receivedBuffer = Memory.Empty; + // 增加数据库处理适配器 + client.SetDataHandler(new FixLengthDataPackageHandler(7) + { + ReceivedCallBack = buffer => + { + receivedBuffer = buffer; + tcs.SetResult(); + return Task.CompletedTask; + } + }); + + // 测试 SendAsync 方法 + var data = new Memory([1, 2, 3, 4, 5]); + var result = await client.SendAsync(data); + Assert.True(result); + + await tcs.Task; + Assert.Equal(receivedBuffer.ToArray(), [1, 2, 3, 4, 5, 3, 4]); + StopTcpServer(server); + } + + private static TcpListener StartTcpServer(int port = 8888) + { + var server = new TcpListener(IPAddress.Loopback, port); + server.Start(); + Task.Run(AcceptClientsAsync); + return server; + + async Task AcceptClientsAsync() + { + while (true) + { + var client = await server.AcceptTcpClientAsync(); + _ = Task.Run(() => HandleClientAsync(client)); + } + } + + async Task HandleClientAsync(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 Memory(buffer, 0, len); + await stream.WriteAsync(block, CancellationToken.None); + } + } + } + + private static TcpListener StartDataPackageTcpServer(int port = 8899) + { + var server = new TcpListener(IPAddress.Loopback, port); server.Start(); Task.Run(AcceptClientsAsync); return server; @@ -98,6 +180,12 @@ async Task HandleClientAsync(TcpClient client) // 回写数据到客户端 var block = new Memory(buffer, 0, len); await stream.WriteAsync(block, CancellationToken.None); + + // 模拟延时 + await Task.Delay(50); + + // 模拟拆包发送第二段数据 + await stream.WriteAsync(new byte[2] { 0x3, 0x4 }, CancellationToken.None); } } } From 8802cdf0dc038662b00b8975b574a5c5794490ed Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 08:28:58 +0800 Subject: [PATCH 21/37] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=E6=8E=A5?= =?UTF-8?q?=E6=94=B6=E4=BB=BB=E5=8A=A1=E5=8F=96=E6=B6=88=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index b15f800799d..f5300b78749 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -16,7 +16,6 @@ class DefaultTcpSocketClient : ITcpSocketClient { private TcpClient? _client; private IDataPackageHandler? _dataPackageHandler; - private Task? _receiveTask; private CancellationTokenSource? _receiveCancellationTokenSource; public bool IsConnected => _client?.Connected ?? false; @@ -60,7 +59,7 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke await _client.ConnectAsync(endPoint, token); // 开始接收数据 - _ = Task.Run(ReceiveAsync); + _ = Task.Run(ReceiveAsync, token); ret = true; } catch (OperationCanceledException ex) @@ -170,6 +169,15 @@ private void Dispose(bool disposing) { if (disposing) { + // 取消接收数据的任务 + if (_receiveCancellationTokenSource is not null) + { + _receiveCancellationTokenSource.Cancel(); + _receiveCancellationTokenSource.Dispose(); + _receiveCancellationTokenSource = null; + } + + // 释放 TcpClient 资源 if (_client != null) { _client.Close(); From cf5fa858ecd48bcfc0538cc8db6be2bfeee82d88 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 08:29:06 +0800 Subject: [PATCH 22/37] =?UTF-8?q?refactor:=20=E7=B2=BE=E7=AE=80=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataPackage/FixLengthDataPackageHandler.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs index 4812eb02ce2..5fc60627424 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs @@ -3,8 +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 System.Drawing; - namespace BootstrapBlazor.Components; /// @@ -27,13 +25,6 @@ public class FixLengthDataPackageHandler(int length) : DataPackageHandlerBase /// public override async Task ReceiveAsync(Memory data) { - if (_receivedLength == 0) - { - data.CopyTo(_data); - _receivedLength = data.Length; - return; - } - // 拷贝数据 var len = length - _receivedLength; var segment = data.Length > len ? data[..len] : data; @@ -45,9 +36,11 @@ public override async Task ReceiveAsync(Memory data) // 如果已接收长度等于总长度则触发回调 if (_receivedLength == length) { + // 重置已接收长度 + _receivedLength = 0; if (ReceivedCallBack != null) { - await ReceivedCallBack(_data); + await ReceivedCallBack(data); } } } From f20cf3efb6202110d4f1ead3f9e7f469d1e0fc89 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 08:38:41 +0800 Subject: [PATCH 23/37] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DataPackage/FixLengthDataPackageHandler.cs | 2 +- test/UnitTest/Services/TcpSocketFactoryTest.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs index 5fc60627424..751065ec049 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs @@ -40,7 +40,7 @@ public override async Task ReceiveAsync(Memory data) _receivedLength = 0; if (ReceivedCallBack != null) { - await ReceivedCallBack(data); + await ReceivedCallBack(_data); } } } diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index cc232c57ec9..df543bd456f 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -111,6 +111,11 @@ public async Task FixLengthDataPackageHandler_Ok() await tcs.Task; Assert.Equal(receivedBuffer.ToArray(), [1, 2, 3, 4, 5, 3, 4]); + + // 关闭连接 + await Task.Delay(200); + client.Close(); + await Task.Delay(100); StopTcpServer(server); } From db7d408c04fa71ee80dd3c3066a2b500185c17fc Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 10:24:31 +0800 Subject: [PATCH 24/37] =?UTF-8?q?refactor:=20=E5=AE=9E=E7=8E=B0=E6=8B=86?= =?UTF-8?q?=E5=8C=85=E7=B2=98=E5=8C=85=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataPackage/DataPackageHandlerBase.cs | 65 +++++++++++++++++-- .../FixLengthDataPackageHandler.cs | 8 +++ .../DataPackage/IDataPackageHandler.cs | 12 ++-- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs index 6555217615c..fba75916761 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone +using System.Buffers; + namespace BootstrapBlazor.Components; /// @@ -13,6 +15,8 @@ namespace BootstrapBlazor.Components; /// implementation simply returns the provided data. public abstract class DataPackageHandlerBase : IDataPackageHandler { + private Memory _lastReceiveBuffer = Memory.Empty; + /// /// 当接收数据处理完成后,回调该函数执行接收 /// @@ -33,15 +37,62 @@ public virtual Task> SendAsync(Memory data) } /// - /// Asynchronously receives data and writes it into the specified memory buffer. + /// Processes the received data asynchronously. /// - /// The method does not guarantee that the entire buffer will be filled. The amount of data - /// written depends on the data available to be received. - /// The memory buffer where the received data will be written. The buffer must be large enough to hold the incoming - /// data. - /// A task that represents the asynchronous receive operation. + /// A memory buffer containing the data to be processed. The buffer must not be empty. + /// A task that represents the asynchronous operation. public virtual Task ReceiveAsync(Memory data) { - return Task.FromResult(data); + return Task.CompletedTask; + } + + /// + /// 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 HandlerStickyPackage(Memory buffer, int length) + { + var len = buffer.Length - length; + if (len > 0) + { + using var memoryBlock = MemoryPool.Shared.Rent(len); + buffer[length..].CopyTo(memoryBlock.Memory); + _lastReceiveBuffer = memoryBlock.Memory[..len]; + } + } + + /// + /// 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 Memory ConcatBuffer(Memory buffer) + { + if (_lastReceiveBuffer.IsEmpty) + { + return buffer; + } + + // 计算缓存区长度 + var len = _lastReceiveBuffer.Length + buffer.Length; + + // 申请缓存 + using var memoryBlock = MemoryPool.Shared.Rent(len); + + // 拷贝数据到缓存区 + _lastReceiveBuffer.CopyTo(memoryBlock.Memory); + buffer.CopyTo(memoryBlock.Memory[_lastReceiveBuffer.Length..]); + + // 清空粘包缓存数据 + _lastReceiveBuffer = Memory.Empty; + return memoryBlock.Memory[..len]; } } diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs index 751065ec049..df3b6ff0159 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs @@ -25,11 +25,19 @@ public class FixLengthDataPackageHandler(int length) : DataPackageHandlerBase /// public override async Task ReceiveAsync(Memory data) { + // 处理上次粘包数据 + data = ConcatBuffer(data); + // 拷贝数据 var len = length - _receivedLength; var segment = data.Length > len ? data[..len] : data; segment.CopyTo(_data[_receivedLength..]); + if(data.Length > len) + { + HandlerStickyPackage(data, data.Length - len); + } + // 更新已接收长度 _receivedLength += segment.Length; diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs index edc2904c44a..89c9f13d0be 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs @@ -30,12 +30,12 @@ public interface IDataPackageHandler Task> SendAsync(Memory data); /// - /// Asynchronously receives data and writes it into the specified memory buffer. + /// Asynchronously receives data from a source and writes it into the provided memory buffer. /// - /// The method does not guarantee that the entire buffer will be filled. The amount of data - /// written depends on the data available to be received. - /// The memory buffer where the received data will be written. The buffer must be large enough to hold the incoming - /// data. - /// A task that represents the asynchronous receive operation. + /// 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 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. Task ReceiveAsync(Memory data); } From c498fea74f918146d07649d7ae652a3663748ac6 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 10:24:48 +0800 Subject: [PATCH 25/37] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=20Logger=20=E4=B8=8D=E4=B8=BA=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index f5300b78749..f576da6f222 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -17,11 +17,14 @@ class DefaultTcpSocketClient : ITcpSocketClient private TcpClient? _client; private IDataPackageHandler? _dataPackageHandler; private CancellationTokenSource? _receiveCancellationTokenSource; + private IPEndPoint? _localEndPoint; + private IPEndPoint? _remoteEndPoint; public bool IsConnected => _client?.Connected ?? false; public IPEndPoint LocalEndPoint { get; } + [NotNull] public ILogger? Logger { get; set; } public int ReceiveBufferSize { get; set; } = 1024 * 10; @@ -60,15 +63,18 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke // 开始接收数据 _ = Task.Run(ReceiveAsync, token); + + _localEndPoint = LocalEndPoint; + _remoteEndPoint = endPoint; ret = true; } catch (OperationCanceledException ex) { - LogWarning(ex, $"TCP Socket connect operation was canceled to {endPoint}"); + Logger.LogWarning(ex, "TCP Socket connect operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint); } catch (Exception ex) { - LogError(ex, $"TCP Socket connection failed to {endPoint}"); + Logger.LogError(ex, "TCP Socket connection failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint); } return ret; } @@ -77,7 +83,7 @@ public async Task SendAsync(Memory data, CancellationToken token = d { if (_client is not { Connected: true }) { - throw new InvalidOperationException("TCP Socket is not connected."); + throw new InvalidOperationException($"TCP Socket is not connected {_localEndPoint}."); } var ret = false; @@ -93,11 +99,11 @@ public async Task SendAsync(Memory data, CancellationToken token = d } catch (OperationCanceledException ex) { - LogWarning(ex, $"TCP Socket send operation was canceled to {_client.Client.RemoteEndPoint}"); + Logger.LogWarning(ex, "TCP Socket send operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); } catch (Exception ex) { - LogError(ex, $"TCP Socket send failed to {_client.Client.RemoteEndPoint}"); + Logger.LogError(ex, "TCP Socket send failed from {LocalEndPoint} to {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); } return ret; } @@ -111,7 +117,7 @@ private async Task ReceiveAsync() { if (_client is not { Connected: true }) { - throw new InvalidOperationException("TCP Socket is not connected."); + throw new InvalidOperationException($"TCP Socket is not connected."); } try @@ -121,7 +127,7 @@ private async Task ReceiveAsync() if (len == 0) { // 远端主机关闭链路 - LogInformation($"TCP Socket received {len} data from {_client.Client.RemoteEndPoint}"); + Logger.LogInformation("TCP Socket {LocalEndPoint} received 0 data closed by {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); break; } else @@ -136,11 +142,11 @@ private async Task ReceiveAsync() } catch (OperationCanceledException ex) { - LogWarning(ex, $"TCP Socket receive operation was canceled to {_client.Client.RemoteEndPoint}"); + Logger.LogWarning(ex, "TCP Socket receive operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); } catch (Exception ex) { - LogError(ex, $"TCP Socket receive failed to {_client.Client.RemoteEndPoint}"); + Logger.LogError(ex, "TCP Socket receive failed from {LocalEndPoint} to {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); } } } @@ -150,25 +156,13 @@ public void Close() Dispose(true); } - private void LogInformation(string message) - { - Logger?.LogInformation("{message}", message); - } - - private void LogWarning(Exception ex, string message) - { - Logger?.LogWarning(ex, "{message}", message); - } - - private void LogError(Exception ex, string message) - { - Logger?.LogError(ex, "{message}", message); - } - private void Dispose(bool disposing) { if (disposing) { + _localEndPoint = null; + _remoteEndPoint = null; + // 取消接收数据的任务 if (_receiveCancellationTokenSource is not null) { From 056147df64d12cbcec729787f6d21f3482aa431d Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 10:24:56 +0800 Subject: [PATCH 26/37] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitTest/Services/TcpSocketFactoryTest.cs | 242 ++++++++++-------- 1 file changed, 141 insertions(+), 101 deletions(-) diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index df543bd456f..672290fd180 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -4,7 +4,6 @@ // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using Microsoft.Extensions.Logging; -using System.Buffers; using System.Net; using System.Net.Sockets; @@ -13,10 +12,9 @@ namespace UnitTest.Services; public class TcpSocketFactoryTest { [Fact] - public async Task TcpSocketFactory_Ok() + public void GetOrCreate_Ok() { - var server = StartTcpServer(); - + // 测试 GetOrCreate 方法创建的 Client 销毁后继续 GetOrCreate 得到的对象是否可用 var sc = new ServiceCollection(); sc.AddLogging(builder => { @@ -26,34 +24,41 @@ public async Task TcpSocketFactory_Ok() var provider = sc.BuildServiceProvider(); var factory = provider.GetRequiredService(); - var client = factory.GetOrCreate("localhost", 0); + var client1 = factory.GetOrCreate("localhost", 0); + client1.Close(); - // 测试 ConnectAsync 方法 - var connect = await client.ConnectAsync("localhost", 8888); - Assert.True(connect); - Assert.True(client.IsConnected); + var client2 = factory.GetOrCreate("localhost", 0); + Assert.Equal(client1, client2); + } - // 增加数据库处理适配器 - client.SetDataHandler(new MockDataHandler() - { - //ReceivedCallBack = buffer => - //{ - //} - }); + [Fact] + public async Task ConnectAsync_Cancel() + { + var client = CreateClient(); - // 测试 SendAsync 方法 - var data = new Memory([1, 2, 3, 4, 5]); - var result = await client.SendAsync(data); - Assert.True(result); + // 测试 ConnectAsync 方法连接取消逻辑 + var cst = new CancellationTokenSource(); + cst.Cancel(); + var connect = await client.ConnectAsync("localhost", 9999, cst.Token); + Assert.False(connect); + } - //Assert.Equal(buffer.ToArray(), [1, 2, 3, 4, 5, 1, 2]); - StopTcpServer(server); + [Fact] + public async Task ConnectAsync_Failed() + { + var client = CreateClient(); + + // 测试 ConnectAsync 方法连接失败 + var connect = await client.ConnectAsync("localhost", 9999); + Assert.False(connect); } [Fact] - public void GetOrCreate_Ok() + public async Task FixLengthDataPackageHandler_Ok() { - // 测试 GetOrCreate 方法创建的 Client 销毁后继续 GetOrCreate 得到的对象是否可用 + var port = 8888; + var server = StartTcpServer(port, MockSplitPackageAsync); + var sc = new ServiceCollection(); sc.AddLogging(builder => { @@ -63,17 +68,48 @@ public void GetOrCreate_Ok() var provider = sc.BuildServiceProvider(); var factory = provider.GetRequiredService(); - var client1 = factory.GetOrCreate("localhost", 0); - client1.Close(); + var client = factory.GetOrCreate("localhost", 0); - var client2 = factory.GetOrCreate("localhost", 0); - Assert.Equal(client1, client2); + // 测试 ConnectAsync 方法 + var connect = await client.ConnectAsync("localhost", port); + Assert.True(connect); + Assert.True(client.IsConnected); + + var tcs = new TaskCompletionSource(); + Memory receivedBuffer = Memory.Empty; + + // 增加数据处理适配器 + client.SetDataHandler(new FixLengthDataPackageHandler(7) + { + ReceivedCallBack = buffer => + { + receivedBuffer = buffer; + tcs.SetResult(); + return Task.CompletedTask; + } + }); + + // 测试 SendAsync 方法 + var data = new Memory([1, 2, 3, 4, 5]); + var result = await client.SendAsync(data); + Assert.True(result); + + await tcs.Task; + Assert.Equal(receivedBuffer.ToArray(), [1, 2, 3, 4, 5, 3, 4]); + + // 模拟延时等待内部继续读取逻辑完成,测试内部 _receiveCancellationTokenSource 取消逻辑 + await Task.Delay(10); + + // 关闭连接 + client.Close(); + StopTcpServer(server); } [Fact] - public async Task FixLengthDataPackageHandler_Ok() + public async Task FixLengthDataPackageHandler_Sticky() { - var server = StartDataPackageTcpServer(); + var port = 8899; + var server = StartTcpServer(port, MockStickyPackageAsync); var sc = new ServiceCollection(); sc.AddLogging(builder => @@ -86,13 +122,12 @@ public async Task FixLengthDataPackageHandler_Ok() var factory = provider.GetRequiredService(); var client = factory.GetOrCreate("localhost", 0); - // 测试 ConnectAsync 方法 - var connect = await client.ConnectAsync("localhost", 8899); - Assert.True(connect); - Assert.True(client.IsConnected); + // 连接 TCP Server + var connect = await client.ConnectAsync("localhost", port); var tcs = new TaskCompletionSource(); Memory receivedBuffer = Memory.Empty; + // 增加数据库处理适配器 client.SetDataHandler(new FixLengthDataPackageHandler(7) { @@ -104,94 +139,97 @@ public async Task FixLengthDataPackageHandler_Ok() } }); - // 测试 SendAsync 方法 + // 发送数据 var data = new Memory([1, 2, 3, 4, 5]); - var result = await client.SendAsync(data); - Assert.True(result); + await client.SendAsync(data); + // 等待接收数据处理完成 await tcs.Task; + + // 验证接收到的数据 Assert.Equal(receivedBuffer.ToArray(), [1, 2, 3, 4, 5, 3, 4]); + // 等待第二次数据 + receivedBuffer = Memory.Empty; + tcs = new TaskCompletionSource(); + await tcs.Task; + + // 验证第二次收到的数据 + Assert.Equal(receivedBuffer.ToArray(), [1, 2, 3, 4, 5, 6, 7]); + // 关闭连接 - await Task.Delay(200); client.Close(); - await Task.Delay(100); StopTcpServer(server); } - private static TcpListener StartTcpServer(int port = 8888) + private static TcpListener StartTcpServer(int port, Func handler) { var server = new TcpListener(IPAddress.Loopback, port); server.Start(); - Task.Run(AcceptClientsAsync); + Task.Run(() => AcceptClientsAsync(server, handler)); return server; + } - async Task AcceptClientsAsync() + private static async Task AcceptClientsAsync(TcpListener server, Func handler) + { + while (true) { - while (true) - { - var client = await server.AcceptTcpClientAsync(); - _ = Task.Run(() => HandleClientAsync(client)); - } + var client = await server.AcceptTcpClientAsync(); + _ = Task.Run(() => handler(client)); } + } - async Task HandleClientAsync(TcpClient client) + private static async Task MockSplitPackageAsync(TcpClient client) + { + using var stream = client.GetStream(); + while (true) { - using var stream = client.GetStream(); - while (true) + var buffer = new byte[10240]; + var len = await stream.ReadAsync(buffer); + if (len == 0) { - var buffer = new byte[10240]; - var len = await stream.ReadAsync(buffer); - if (len == 0) - { - break; - } - - // 回写数据到客户端 - var block = new Memory(buffer, 0, len); - await stream.WriteAsync(block, CancellationToken.None); + break; } + + // 回写数据到客户端 + var block = new Memory(buffer, 0, len); + await stream.WriteAsync(block, CancellationToken.None); + + // 模拟延时 + await Task.Delay(50); + + // 模拟拆包发送第二段数据 + await stream.WriteAsync(new byte[] { 0x3, 0x4 }, CancellationToken.None); } } - private static TcpListener StartDataPackageTcpServer(int port = 8899) + private static async Task MockStickyPackageAsync(TcpClient client) { - var server = new TcpListener(IPAddress.Loopback, port); - server.Start(); - Task.Run(AcceptClientsAsync); - return server; - - async Task AcceptClientsAsync() + using var stream = client.GetStream(); + while (true) { - while (true) + var buffer = new byte[10240]; + var len = await stream.ReadAsync(buffer); + if (len == 0) { - var client = await server.AcceptTcpClientAsync(); - _ = Task.Run(() => HandleClientAsync(client)); + break; } - } - async Task HandleClientAsync(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 Memory(buffer, 0, len); - await stream.WriteAsync(block, CancellationToken.None); - - // 模拟延时 - await Task.Delay(50); - - // 模拟拆包发送第二段数据 - await stream.WriteAsync(new byte[2] { 0x3, 0x4 }, CancellationToken.None); - } + // 回写数据到客户端 + var block = new Memory(buffer, 0, len); + await stream.WriteAsync(block, CancellationToken.None); + + // 模拟延时 + await Task.Delay(50); + + // 模拟拆包发送第二段数据 + await stream.WriteAsync(new byte[] { 0x3, 0x4, 0x1, 0x2 }, CancellationToken.None); + + // 模拟延时 + await Task.Delay(50); + + // 模拟粘包发送后续数据 + await stream.WriteAsync(new byte[] { 0x3, 0x4, 0x5, 0x6, 0x7 }, CancellationToken.None); } } @@ -200,16 +238,18 @@ private static void StopTcpServer(TcpListener server) server?.Stop(); } - class MockDataHandler : DataPackageHandlerBase + private static ITcpSocketClient CreateClient() { - public override Task> ReceiveAsync(Memory data) + var sc = new ServiceCollection(); + sc.AddLogging(builder => { - using var buffer = MemoryPool.Shared.Rent(data.Length + 2); - data.CopyTo(buffer.Memory); - buffer.Memory.Span[data.Length] = 0x01; - buffer.Memory.Span[data.Length + 1] = 0x02; - return Task.FromResult(buffer.Memory[..(data.Length + 2)]); - } + builder.AddProvider(new MockLoggerProvider()); + }); + sc.AddBootstrapBlazorTcpSocketFactory(); + var provider = sc.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + var client = factory.GetOrCreate("localhost", 0); + return client; } class MockLoggerProvider : ILoggerProvider From 9567cf985dd6bda0af1feb08bfb7e1f4bd676c6d Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 10:56:35 +0800 Subject: [PATCH 27/37] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=20SendAsync=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 20 +++--- .../UnitTest/Services/TcpSocketFactoryTest.cs | 64 +++++++++++++++++++ 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index f576da6f222..c9221d107dc 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -17,12 +17,11 @@ class DefaultTcpSocketClient : ITcpSocketClient private TcpClient? _client; private IDataPackageHandler? _dataPackageHandler; private CancellationTokenSource? _receiveCancellationTokenSource; - private IPEndPoint? _localEndPoint; private IPEndPoint? _remoteEndPoint; public bool IsConnected => _client?.Connected ?? false; - public IPEndPoint LocalEndPoint { get; } + public IPEndPoint LocalEndPoint { get; set; } [NotNull] public ILogger? Logger { get; set; } @@ -64,7 +63,7 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke // 开始接收数据 _ = Task.Run(ReceiveAsync, token); - _localEndPoint = LocalEndPoint; + LocalEndPoint = (IPEndPoint)_client.Client.LocalEndPoint!; _remoteEndPoint = endPoint; ret = true; } @@ -83,7 +82,7 @@ public async Task SendAsync(Memory data, CancellationToken token = d { if (_client is not { Connected: true }) { - throw new InvalidOperationException($"TCP Socket is not connected {_localEndPoint}."); + throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); } var ret = false; @@ -99,11 +98,11 @@ public async Task SendAsync(Memory data, CancellationToken token = d } catch (OperationCanceledException ex) { - Logger.LogWarning(ex, "TCP Socket send operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); + Logger.LogWarning(ex, "TCP Socket send operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); } catch (Exception ex) { - Logger.LogError(ex, "TCP Socket send failed from {LocalEndPoint} to {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); + Logger.LogError(ex, "TCP Socket send failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); } return ret; } @@ -117,7 +116,7 @@ private async Task ReceiveAsync() { if (_client is not { Connected: true }) { - throw new InvalidOperationException($"TCP Socket is not connected."); + throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); } try @@ -127,7 +126,7 @@ private async Task ReceiveAsync() if (len == 0) { // 远端主机关闭链路 - Logger.LogInformation("TCP Socket {LocalEndPoint} received 0 data closed by {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); + Logger.LogInformation("TCP Socket {LocalEndPoint} received 0 data closed by {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); break; } else @@ -142,11 +141,11 @@ private async Task ReceiveAsync() } catch (OperationCanceledException ex) { - Logger.LogWarning(ex, "TCP Socket receive operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); + Logger.LogWarning(ex, "TCP Socket receive operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); } catch (Exception ex) { - Logger.LogError(ex, "TCP Socket receive failed from {LocalEndPoint} to {RemoteEndPoint}", _localEndPoint, _remoteEndPoint); + Logger.LogError(ex, "TCP Socket receive failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); } } } @@ -160,7 +159,6 @@ private void Dispose(bool disposing) { if (disposing) { - _localEndPoint = null; _remoteEndPoint = null; // 取消接收数据的任务 diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index 672290fd180..2b0ca953851 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -53,6 +53,58 @@ public async Task ConnectAsync_Failed() Assert.False(connect); } + [Fact] + public async Task SendAsync_Error() + { + var client = CreateClient(); + + // 测试未建立连接前调用 SendAsync 方法报异常逻辑 + var data = new Memory([1, 2, 3, 4, 5]); + var ex = await Assert.ThrowsAsync(() => client.SendAsync(data)); + Assert.Equal("TCP Socket is not connected 127.0.0.1:0", ex.Message); + } + + [Fact] + public async Task SendAsync_Cancel() + { + var port = 8881; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(); + await client.ConnectAsync("localhost", port); + Assert.True(client.IsConnected); + + // 测试 SendAsync 方法发送取消逻辑 + var cst = new CancellationTokenSource(); + cst.Cancel(); + + var data = new Memory([1, 2, 3, 4, 5]); + var result = await client.SendAsync(data, cst.Token); + Assert.False(result); + + // 设置延时发送适配器 + // 延时发送期间关闭 Socket 连接导致内部报错 + client.SetDataHandler(new MockDelaySendHandler() + { + Socket = client + }); + + var tcs = new TaskCompletionSource(); + bool? sendResult = null; + // 测试发送失败逻辑 + _ = Task.Run(async () => + { + sendResult = await client.SendAsync(data); + tcs.SetResult(); + }); + + await tcs.Task; + Assert.False(sendResult); + + // 关闭连接 + StopTcpServer(server); + } + [Fact] public async Task FixLengthDataPackageHandler_Ok() { @@ -282,4 +334,16 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } } + + class MockDelaySendHandler : DataPackageHandlerBase + { + public ITcpSocketClient? Socket { get; set; } + + public override async Task> SendAsync(Memory data) + { + Socket?.Close(); + await Task.Delay(20); + return data; + } + } } From a772e36f693fd8b5f9669c012d1e27b761b51e50 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 11:46:31 +0800 Subject: [PATCH 28/37] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=20Factory=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 5 +- .../TcpSocket/DefaultTcpSocketFactory.cs | 12 ++- .../Services/TcpSocket/ITcpSocketClient.cs | 5 + .../Services/TcpSocket/ITcpSocketFactory.cs | 14 ++- .../Services/TcpSocket/SocketMode.cs | 22 ----- .../UnitTest/Services/TcpSocketFactoryTest.cs | 94 ++++++++++++++++++- 6 files changed, 122 insertions(+), 30 deletions(-) delete mode 100644 src/BootstrapBlazor/Services/TcpSocket/SocketMode.cs diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index c9221d107dc..c043e498960 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -35,7 +35,10 @@ public DefaultTcpSocketClient(string host, int port = 0) private static IPAddress GetIPAddress(string host) => host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ? IPAddress.Loopback - : IPAddress.TryParse(host, out var ip) ? ip : Dns.GetHostAddresses(host).FirstOrDefault() ?? IPAddress.Loopback; + : IPAddress.TryParse(host, out var ip) ? ip : IPAddressByHostName; + + [ExcludeFromCodeCoverage] + private static IPAddress IPAddressByHostName => Dns.GetHostAddresses(Dns.GetHostName(), AddressFamily.InterNetwork).FirstOrDefault() ?? IPAddress.Loopback; public void SetDataHandler(IDataPackageHandler handler) { diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs index 1ef20409826..8f52e380cd6 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs @@ -15,7 +15,7 @@ class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory { private readonly ConcurrentDictionary _pool = new(); - public ITcpSocketClient GetOrCreate(string host, int port = 0, SocketMode mode = SocketMode.Client) + public ITcpSocketClient GetOrCreate(string host, int port = 0) { return _pool.GetOrAdd($"{host}:{port}", key => { @@ -27,6 +27,16 @@ public ITcpSocketClient GetOrCreate(string host, int port = 0, SocketMode mode = }); } + public ITcpSocketClient? Remove(string host, int port) + { + ITcpSocketClient? client = null; + if (_pool.TryRemove($"{host}:{port}", out var c)) + { + client = c; + } + return client; + } + private void Dispose(bool disposing) { if (disposing) diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index cac839f5bb4..3c89a34b1db 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -12,6 +12,11 @@ namespace BootstrapBlazor.Components; /// public interface ITcpSocketClient : IDisposable { + /// + /// Gets or sets the size, in bytes, of the receive buffer used for network operations. + /// + int ReceiveBufferSize { get; set; } + /// /// Gets a value indicating whether the system is currently connected. /// diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs index efa67e26276..b72069e2f43 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs @@ -16,8 +16,16 @@ public interface ITcpSocketFactory : IDisposable /// /// The hostname or IP address of the remote endpoint. Cannot be null or empty. /// The port number of the remote endpoint. Must be a valid port number between 0 and 65535. - /// The mode of the socket, specifying whether it operates as a client or server. Defaults to . /// An instance representing the TCP socket for the specified host and port. - ITcpSocketClient GetOrCreate(string host, int port, SocketMode mode = SocketMode.Client); + ITcpSocketClient GetOrCreate(string host, int port); + + /// + /// Removes the specified host and port combination from the collection. + /// + /// If the specified host and port combination does not exist in the collection, the method has + /// no effect. + /// The hostname to remove. Cannot be null or empty. + /// The port number associated with the host to remove. Must be a valid port number (0-65535). + /// An instance representing the TCP socket for the specified host and port. + ITcpSocketClient? Remove(string host, int port); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/SocketMode.cs b/src/BootstrapBlazor/Services/TcpSocket/SocketMode.cs deleted file mode 100644 index fdf2057abe1..00000000000 --- a/src/BootstrapBlazor/Services/TcpSocket/SocketMode.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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; - -/// -/// Socket 工作模式 -/// -public enum SocketMode -{ - /// - /// Client 模式 - /// - Client, - - /// - /// Server 模式 - /// - Server -} diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index 2b0ca953851..2bafa36eff7 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -29,6 +29,17 @@ public void GetOrCreate_Ok() var client2 = factory.GetOrCreate("localhost", 0); Assert.Equal(client1, client2); + + var ip = Dns.GetHostAddresses(Dns.GetHostName(), AddressFamily.InterNetwork).FirstOrDefault() ?? IPAddress.Loopback; + var client3 = factory.GetOrCreate(ip.ToString(), 0); + + // 测试不合格 IP 地址 + var client4 = factory.GetOrCreate("256.0.0.1", 0); + + var client5 = factory.Remove("256.0.0.1", 0); + Assert.Equal(client4, client5); + + factory.Dispose(); } [Fact] @@ -71,6 +82,9 @@ public async Task SendAsync_Cancel() var server = StartTcpServer(port, MockSplitPackageAsync); var client = CreateClient(); + Assert.False(client.IsConnected); + + // 连接 TCP Server await client.ConnectAsync("localhost", port); Assert.True(client.IsConnected); @@ -84,7 +98,7 @@ public async Task SendAsync_Cancel() // 设置延时发送适配器 // 延时发送期间关闭 Socket 连接导致内部报错 - client.SetDataHandler(new MockDelaySendHandler() + client.SetDataHandler(new MockSendErrorHandler() { Socket = client }); @@ -105,6 +119,60 @@ public async Task SendAsync_Cancel() StopTcpServer(server); } + [Fact] + public async Task ReceiveAsync_Error() + { + var client = CreateClient(); + + // 测试未建立连接前调用 ReceiveAsync 方法报异常逻辑 + var methodInfo = client.GetType().GetMethod("ReceiveAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(methodInfo); + + var task = (Task)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 * 10, client.ReceiveBufferSize); + + client.ReceiveBufferSize = 1024 * 20; + Assert.Equal(1024 * 20, client.ReceiveBufferSize); + + client.SetDataHandler(new MockReceiveErrorHandler()); + await client.ConnectAsync("localhost", port); + + // 发送数据导致接收数据异常 + var data = new Memory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + client.Dispose(); + + // 关闭连接 + StopTcpServer(server); + } + + [Fact] + public async Task CloseByRemote_Ok() + { + var client = CreateClient(); + + var port = 8883; + var server = StartTcpServer(port, MockAutoClosePackageAsync); + + client.SetDataHandler(new MockReceiveErrorHandler()); + + // 连接 TCP Server + await client.ConnectAsync("localhost", port); + + // 发送数据 + await client.SendAsync(new Memory([1, 2, 3, 4, 5])); + + // 关闭连接 + StopTcpServer(server); + } + [Fact] public async Task FixLengthDataPackageHandler_Ok() { @@ -285,6 +353,12 @@ private static async Task MockStickyPackageAsync(TcpClient client) } } + private static Task MockAutoClosePackageAsync(TcpClient client) + { + client.Close(); + return Task.CompletedTask; + } + private static void StopTcpServer(TcpListener server) { server?.Stop(); @@ -335,15 +409,29 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } } - class MockDelaySendHandler : DataPackageHandlerBase + class MockSendErrorHandler : DataPackageHandlerBase { public ITcpSocketClient? Socket { get; set; } public override async Task> SendAsync(Memory data) { Socket?.Close(); - await Task.Delay(20); + await Task.Delay(10); return data; } } + + class MockReceiveErrorHandler : DataPackageHandlerBase + { + public override Task> SendAsync(Memory data) + { + return Task.FromResult(data); + } + + public override Task ReceiveAsync(Memory data) + { + // 模拟接收数据时报错 + throw new InvalidOperationException("Test Error"); + } + } } From 2b23292fac4818c7a921191778aa86450c0b950a Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 11:56:39 +0800 Subject: [PATCH 29/37] =?UTF-8?q?test:=20=E7=B2=BE=E7=AE=80=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitTest/Services/TcpSocketFactoryTest.cs | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index 2bafa36eff7..cf3fe492878 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -38,6 +38,9 @@ public void GetOrCreate_Ok() var client5 = factory.Remove("256.0.0.1", 0); Assert.Equal(client4, client5); + Assert.NotNull(client5); + + client5.Dispose(); factory.Dispose(); } @@ -147,8 +150,6 @@ public async Task ReceiveAsync_Error() var data = new Memory([1, 2, 3, 4, 5]); await client.SendAsync(data); - client.Dispose(); - // 关闭连接 StopTcpServer(server); } @@ -178,17 +179,7 @@ public async Task FixLengthDataPackageHandler_Ok() { var port = 8888; var server = StartTcpServer(port, MockSplitPackageAsync); - - var sc = new ServiceCollection(); - sc.AddLogging(builder => - { - builder.AddProvider(new MockLoggerProvider()); - }); - sc.AddBootstrapBlazorTcpSocketFactory(); - - var provider = sc.BuildServiceProvider(); - var factory = provider.GetRequiredService(); - var client = factory.GetOrCreate("localhost", 0); + var client = CreateClient(); // 测试 ConnectAsync 方法 var connect = await client.ConnectAsync("localhost", port); @@ -230,17 +221,7 @@ public async Task FixLengthDataPackageHandler_Sticky() { var port = 8899; var server = StartTcpServer(port, MockStickyPackageAsync); - - var sc = new ServiceCollection(); - sc.AddLogging(builder => - { - builder.AddProvider(new MockLoggerProvider()); - }); - sc.AddBootstrapBlazorTcpSocketFactory(); - - var provider = sc.BuildServiceProvider(); - var factory = provider.GetRequiredService(); - var client = factory.GetOrCreate("localhost", 0); + var client = CreateClient(); // 连接 TCP Server var connect = await client.ConnectAsync("localhost", port); @@ -428,8 +409,10 @@ public override Task> SendAsync(Memory data) return Task.FromResult(data); } - public override Task ReceiveAsync(Memory data) + public override async Task ReceiveAsync(Memory data) { + await base.ReceiveAsync(data); + // 模拟接收数据时报错 throw new InvalidOperationException("Test Error"); } From a9eb67f14ad10766cae80483bfdb13b8891e68f1 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 12:01:38 +0800 Subject: [PATCH 30/37] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=20IsWasm=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HostEnvironmentExtensionsTest.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/UnitTest/Extensions/HostEnvironmentExtensionsTest.cs diff --git a/test/UnitTest/Extensions/HostEnvironmentExtensionsTest.cs b/test/UnitTest/Extensions/HostEnvironmentExtensionsTest.cs new file mode 100644 index 00000000000..1f6f0652ad9 --- /dev/null +++ b/test/UnitTest/Extensions/HostEnvironmentExtensionsTest.cs @@ -0,0 +1,30 @@ +// 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.FileProviders; +using Microsoft.Extensions.Hosting; + +namespace UnitTest.Extensions; + +public class HostEnvironmentExtensionsTest +{ + [Fact] + public void IsWasm_Ok() + { + var hostEnvironment = new MockWasmHostEnvironment(); + Assert.False(hostEnvironment.IsWasm()); + } + + class MockWasmHostEnvironment : IHostEnvironment + { + public string EnvironmentName { get; set; } = "Development"; + + public string ApplicationName { get; set; } = "BootstrapBlazor"; + + public string ContentRootPath { get; set; } = ""; + + public IFileProvider ContentRootFileProvider { get; set; } = null!; + } +} From 44e4bd376888875d43c33d02906735e8354e8080 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 13:10:32 +0800 Subject: [PATCH 31/37] =?UTF-8?q?refactor:=20=E6=8E=A5=E6=94=B6=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=86=85=E5=BC=82=E5=B8=B8=E6=94=B9=E4=B8=BA=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index c043e498960..6d87a64cf69 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -119,7 +119,8 @@ private async Task ReceiveAsync() { if (_client is not { Connected: true }) { - throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); + Logger.LogError("TCP Socket is not connected {LocalEndPoint}", LocalEndPoint); + break; } try From efdd44771cf0073dd08199dafdf0dd450321743a Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 13:15:38 +0800 Subject: [PATCH 32/37] =?UTF-8?q?refactor:=20=E9=98=B2=E6=AD=A2=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E5=8C=BA=E8=A2=AB=E9=87=8A=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs index fba75916761..c580c2c35fb 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs @@ -59,7 +59,7 @@ protected void HandlerStickyPackage(Memory buffer, int length) var len = buffer.Length - length; if (len > 0) { - using var memoryBlock = MemoryPool.Shared.Rent(len); + var memoryBlock = MemoryPool.Shared.Rent(len); buffer[length..].CopyTo(memoryBlock.Memory); _lastReceiveBuffer = memoryBlock.Memory[..len]; } @@ -85,7 +85,7 @@ protected Memory ConcatBuffer(Memory buffer) var len = _lastReceiveBuffer.Length + buffer.Length; // 申请缓存 - using var memoryBlock = MemoryPool.Shared.Rent(len); + var memoryBlock = MemoryPool.Shared.Rent(len); // 拷贝数据到缓存区 _lastReceiveBuffer.CopyTo(memoryBlock.Memory); From e4d04f70c4e214726d002b5653dcf8c672b1d5f8 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 13:31:10 +0800 Subject: [PATCH 33/37] =?UTF-8?q?refactor:=20=E7=B2=BE=E7=AE=80=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=8F=90=E9=AB=98=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataPackage/DataPackageHandlerBase.cs | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs index c580c2c35fb..e4a3c53728d 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs @@ -3,8 +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 System.Buffers; - namespace BootstrapBlazor.Components; /// @@ -56,12 +54,9 @@ public virtual Task ReceiveAsync(Memory data) /// The length of the valid data within the buffer. protected void HandlerStickyPackage(Memory buffer, int length) { - var len = buffer.Length - length; - if (len > 0) + if (buffer.Length > length) { - var memoryBlock = MemoryPool.Shared.Rent(len); - buffer[length..].CopyTo(memoryBlock.Memory); - _lastReceiveBuffer = memoryBlock.Memory[..len]; + _lastReceiveBuffer = buffer[length..].ToArray().AsMemory(); } } @@ -82,17 +77,13 @@ protected Memory ConcatBuffer(Memory buffer) } // 计算缓存区长度 - var len = _lastReceiveBuffer.Length + buffer.Length; - - // 申请缓存 - var memoryBlock = MemoryPool.Shared.Rent(len); - - // 拷贝数据到缓存区 - _lastReceiveBuffer.CopyTo(memoryBlock.Memory); - buffer.CopyTo(memoryBlock.Memory[_lastReceiveBuffer.Length..]); + var total = _lastReceiveBuffer.Length + buffer.Length; + var merged = new byte[total]; + _lastReceiveBuffer.CopyTo(merged); + buffer.CopyTo(merged.AsMemory(_lastReceiveBuffer.Length)); - // 清空粘包缓存数据 + // Clear the sticky buffer _lastReceiveBuffer = Memory.Empty; - return memoryBlock.Memory[..len]; + return merged; } } From b6e0e6f25db712263a780b909b6a5660798498b8 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 14:15:32 +0800 Subject: [PATCH 34/37] =?UTF-8?q?Revert=20"refactor:=20=E6=8E=A5=E6=94=B6?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=86=85=E5=BC=82=E5=B8=B8=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=97=A5=E5=BF=97"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 44e4bd376888875d43c33d02906735e8354e8080. --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index 6d87a64cf69..c043e498960 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -119,8 +119,7 @@ private async Task ReceiveAsync() { if (_client is not { Connected: true }) { - Logger.LogError("TCP Socket is not connected {LocalEndPoint}", LocalEndPoint); - break; + throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); } try From fc13b5f32eaa6efff042df987813bc09a4488884 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 14:23:16 +0800 Subject: [PATCH 35/37] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=AD=A3=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=90=8D=E7=A7=B0=E4=B8=BA=20HandleStickyPackage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs | 2 +- .../TcpSocket/DataPackage/FixLengthDataPackageHandler.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs index e4a3c53728d..953ecb74d55 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs @@ -52,7 +52,7 @@ public virtual Task ReceiveAsync(Memory data) /// for the specified . /// The memory buffer containing the data to process. /// The length of the valid data within the buffer. - protected void HandlerStickyPackage(Memory buffer, int length) + protected void HandleStickyPackage(Memory buffer, int length) { if (buffer.Length > length) { diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs index df3b6ff0159..03d2dc2fe73 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs @@ -35,7 +35,7 @@ public override async Task ReceiveAsync(Memory data) if(data.Length > len) { - HandlerStickyPackage(data, data.Length - len); + HandleStickyPackage(data, data.Length - len); } // 更新已接收长度 From 6ea1b2539e8750feaf859e63ef04cf9889a0acf3 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 14:32:41 +0800 Subject: [PATCH 36/37] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=94=B9=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=E7=BC=93=E5=AD=98=E5=8C=BA=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index c043e498960..3701ac82f18 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -112,8 +112,6 @@ public async Task SendAsync(Memory data, CancellationToken token = d private async Task ReceiveAsync() { - using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); - var buffer = block.Memory; _receiveCancellationTokenSource ??= new(); while (_receiveCancellationTokenSource is { IsCancellationRequested: false }) { @@ -124,6 +122,8 @@ private async Task ReceiveAsync() try { + using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); + var buffer = block.Memory; var stream = _client.GetStream(); var len = await stream.ReadAsync(buffer, _receiveCancellationTokenSource.Token); if (len == 0) From ef65e62edc6b4dd15a2d1cefb15df1e57a6d0ae1 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 14:39:05 +0800 Subject: [PATCH 37/37] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=8B=86?= =?UTF-8?q?=E5=8C=85=E6=96=B9=E6=B3=95=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DataPackage/DataPackageHandlerBase.cs | 12 ++++-------- .../DataPackage/FixLengthDataPackageHandler.cs | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs index 953ecb74d55..cba5c6e3156 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs @@ -52,12 +52,9 @@ public virtual Task ReceiveAsync(Memory data) /// for the specified . /// The memory buffer containing the data to process. /// The length of the valid data within the buffer. - protected void HandleStickyPackage(Memory buffer, int length) + protected void SlicePackage(Memory buffer, int length) { - if (buffer.Length > length) - { - _lastReceiveBuffer = buffer[length..].ToArray().AsMemory(); - } + _lastReceiveBuffer = buffer[length..].ToArray().AsMemory(); } /// @@ -77,10 +74,9 @@ protected Memory ConcatBuffer(Memory buffer) } // 计算缓存区长度 - var total = _lastReceiveBuffer.Length + buffer.Length; - var merged = new byte[total]; + Memory merged = new byte[_lastReceiveBuffer.Length + buffer.Length]; _lastReceiveBuffer.CopyTo(merged); - buffer.CopyTo(merged.AsMemory(_lastReceiveBuffer.Length)); + buffer.CopyTo(merged[_lastReceiveBuffer.Length..]); // Clear the sticky buffer _lastReceiveBuffer = Memory.Empty; diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs index 03d2dc2fe73..3efbe4dee7b 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs @@ -33,9 +33,9 @@ public override async Task ReceiveAsync(Memory data) var segment = data.Length > len ? data[..len] : data; segment.CopyTo(_data[_receivedLength..]); - if(data.Length > len) + if (data.Length > len) { - HandleStickyPackage(data, data.Length - len); + SlicePackage(data, data.Length - len); } // 更新已接收长度