From a36c91009ab125dea810c929812471f699e9ee0e Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 27 Jun 2025 08:50:40 +0800 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E9=94=80=E6=AF=81=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index 695db43849c..56de2401318 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -10,7 +10,7 @@ namespace BootstrapBlazor.Components; /// /// Represents a TCP socket for network communication. /// -public interface ITcpSocketClient : IDisposable +public interface ITcpSocketClient : IAsyncDisposable { /// /// Gets or sets the size, in bytes, of the receive buffer used for network operations. @@ -110,5 +110,5 @@ public interface ITcpSocketClient : IDisposable /// 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(); + ValueTask Close(); } From 22750ca702a40163f10b3752ebc4c449257b418e Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 27 Jun 2025 08:50:55 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TcpSocket/DefaultTcpSocketClient.cs | 45 ++----- .../Services/TcpSocket/TcpSocketClientBase.cs | 123 ++++++++++++++++++ 2 files changed, 132 insertions(+), 36 deletions(-) create mode 100644 src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index d93712bb46d..c824465aecd 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -12,44 +12,30 @@ namespace BootstrapBlazor.Components; [UnsupportedOSPlatform("browser")] -sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : ITcpSocketClient +sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : TcpSocketClientBase { private TcpClient? _client; private IDataPackageHandler? _dataPackageHandler; private CancellationTokenSource? _receiveCancellationTokenSource; private IPEndPoint? _remoteEndPoint; - public bool IsConnected => _client?.Connected ?? false; - - public IPEndPoint? LocalEndPoint { get; set; } + public override bool IsConnected => _client?.Connected ?? false; [NotNull] public ILogger? Logger { get; set; } - public int ReceiveBufferSize { get; set; } = 1024 * 64; - - public bool IsAutoReceive { get; set; } = true; - - public Func, ValueTask>? ReceivedCallBack { get; set; } - - public int ConnectTimeout { get; set; } - - public int SendTimeout { get; set; } - - public int ReceiveTimeout { get; set; } - - public void SetDataHandler(IDataPackageHandler handler) + public override void SetDataHandler(IDataPackageHandler handler) { _dataPackageHandler = handler; } - public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + public override async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) { var ret = false; try { // 释放资源 - Close(); + await Close(); // 创建新的 TcpClient 实例 _client ??= new TcpClient(localEndPoint); @@ -91,7 +77,7 @@ public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken return ret; } - public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + public override async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) { if (_client is not { Connected: true }) { @@ -137,7 +123,7 @@ public async ValueTask SendAsync(ReadOnlyMemory data, CancellationTo return ret; } - public async ValueTask> ReceiveAsync(CancellationToken token = default) + public override async ValueTask> ReceiveAsync(CancellationToken token = default) { if (_client == null || !_client.Connected) { @@ -228,12 +214,7 @@ private async ValueTask ReceiveCoreAsync(TcpClient client, Memory buf return len; } - public void Close() - { - Dispose(true); - } - - private void Dispose(bool disposing) + protected override ValueTask DisposeAsync(bool disposing) { if (disposing) { @@ -255,14 +236,6 @@ private void Dispose(bool disposing) _client = null; } } - } - - /// - /// - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); + return ValueTask.CompletedTask; } } diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs new file mode 100644 index 00000000000..45184f740ea --- /dev/null +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs @@ -0,0 +1,123 @@ +// 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; + +namespace BootstrapBlazor.Components; + +/// +/// Provides a base implementation for a TCP socket client, enabling connection, data transmission, and reception over +/// TCP. +/// +/// This abstract class serves as a foundation for implementing TCP socket clients. It provides methods +/// for connecting to a remote endpoint, sending and receiving data, and managing connection state. Derived classes can +/// extend or customize the behavior as needed. +public abstract class TcpSocketClientBase : ITcpSocketClient +{ + /// + /// + /// + public abstract bool IsConnected { get; } + + /// + /// + /// + public IPEndPoint? LocalEndPoint { get; set; } + + /// + /// + /// + public int ReceiveBufferSize { get; set; } = 1024 * 64; + + /// + /// + /// + public bool IsAutoReceive { get; set; } = true; + + /// + /// + /// + public Func, ValueTask>? ReceivedCallBack { get; set; } + + /// + /// + /// + public int ConnectTimeout { get; set; } + + /// + /// + /// + public int SendTimeout { get; set; } + + /// + /// + /// + public int ReceiveTimeout { get; set; } + + /// + /// + /// + public virtual void SetDataHandler(IDataPackageHandler handler) + { + + } + + /// + /// + /// + /// + /// + /// + public abstract ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default); + + /// + /// + /// + /// + /// + /// + public abstract ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default); + + /// + /// + /// + /// + /// + public abstract ValueTask> ReceiveAsync(CancellationToken token = default); + + /// + /// + /// + public virtual ValueTask Close() + { + return DisposeAsync(true); + } + + /// + /// Releases the resources used by the current instance of the class. + /// + /// This method is called to free both managed and unmanaged resources. If the parameter is , the method releases managed resources in addition to + /// unmanaged resources. Override this method in a derived class to provide custom cleanup logic. + /// to release both managed and unmanaged resources; to release only + /// unmanaged resources. + protected virtual ValueTask DisposeAsync(bool disposing) + { + if (disposing) + { + LocalEndPoint = null; + } + return ValueTask.CompletedTask; + } + + /// + /// + /// + public async ValueTask DisposeAsync() + { + await DisposeAsync(true); + GC.SuppressFinalize(this); + } +} From d9dde094fe04f4c2ca6de076277eca3135640e4b Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 27 Jun 2025 08:56:37 +0800 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=94=B9=20Close=20?= =?UTF-8?q?=E4=B8=BA=20CloseAsync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 2 +- .../Services/TcpSocket/DefaultTcpSocketFactory.cs | 8 ++++---- .../Services/TcpSocket/ITcpSocketClient.cs | 2 +- .../Services/TcpSocket/ITcpSocketFactory.cs | 2 +- .../Services/TcpSocket/TcpSocketClientBase.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index c824465aecd..7459797fc03 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -35,7 +35,7 @@ public override async ValueTask ConnectAsync(IPEndPoint endPoint, Cancella try { // 释放资源 - await Close(); + await CloseAsync(); // 创建新的 TcpClient 实例 _client ??= new TcpClient(localEndPoint); diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs index 24296aa587e..3096441d04a 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs @@ -39,14 +39,14 @@ public ITcpSocketClient GetOrCreate(string name, Func valueF return client; } - private void Dispose(bool disposing) + private async ValueTask DisposeAsync(bool disposing) { if (disposing) { // 释放托管资源 foreach (var socket in _pool.Values) { - socket.Dispose(); + await socket.DisposeAsync(); } _pool.Clear(); } @@ -55,9 +55,9 @@ private void Dispose(bool disposing) /// /// /// - public void Dispose() + public async ValueTask DisposeAsync() { - Dispose(true); + await DisposeAsync(true); GC.SuppressFinalize(this); } } diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index 56de2401318..92bb9591378 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -110,5 +110,5 @@ public interface ITcpSocketClient : IAsyncDisposable /// 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. - ValueTask Close(); + ValueTask CloseAsync(); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs index 44db6076ca1..e1d4aa35720 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs @@ -10,7 +10,7 @@ namespace BootstrapBlazor.Components; /// /// ITcpSocketFactory Interface /// -public interface ITcpSocketFactory : IDisposable +public interface ITcpSocketFactory : IAsyncDisposable { /// /// Retrieves an existing TCP socket client by name or creates a new one using the specified factory function. diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs index 45184f740ea..5e17b5f4b6a 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs @@ -90,7 +90,7 @@ public virtual void SetDataHandler(IDataPackageHandler handler) /// /// /// - public virtual ValueTask Close() + public virtual ValueTask CloseAsync() { return DisposeAsync(true); } From c870a62b32fa2507aee00155e6cf9b38097bbd12 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 27 Jun 2025 08:56:45 +0800 Subject: [PATCH 4/6] =?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 | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index a9fade5f4af..dcdcf21e4bd 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -13,7 +13,7 @@ namespace UnitTest.Services; public class TcpSocketFactoryTest { [Fact] - public void GetOrCreate_Ok() + public async Task GetOrCreate_Ok() { // 测试 GetOrCreate 方法创建的 Client 销毁后继续 GetOrCreate 得到的对象是否可用 var sc = new ServiceCollection(); @@ -25,7 +25,7 @@ public void GetOrCreate_Ok() var provider = sc.BuildServiceProvider(); var factory = provider.GetRequiredService(); var client1 = factory.GetOrCreate("demo", key => Utility.ConvertToIpEndPoint("localhost", 0)); - client1.Close(); + await client1.CloseAsync(); var client2 = factory.GetOrCreate("demo", key => Utility.ConvertToIpEndPoint("localhost", 0)); Assert.Equal(client1, client2); @@ -40,8 +40,8 @@ public void GetOrCreate_Ok() Assert.Equal(client4, client5); Assert.NotNull(client5); - client5.Dispose(); - factory.Dispose(); + await client5.DisposeAsync(); + await factory.DisposeAsync(); } [Fact] @@ -211,7 +211,7 @@ public async Task ReceiveAsync_InvalidOperationException() ex = null; ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); - client.Close(); + await client.CloseAsync(); client.IsAutoReceive = false; var connected = await client.ConnectAsync("localhost", port); Assert.True(connected); @@ -342,7 +342,7 @@ public async Task FixLengthDataPackageHandler_Ok() await Task.Delay(10); // 关闭连接 - client.Close(); + await client.CloseAsync(); StopTcpServer(server); } @@ -394,7 +394,7 @@ public async Task FixLengthDataPackageHandler_Sticky() Assert.Equal(receivedBuffer.ToArray(), [3, 2, 3, 4, 5, 6, 7]); // 关闭连接 - client.Close(); + await client.CloseAsync(); StopTcpServer(server); } @@ -441,7 +441,7 @@ public async Task DelimiterDataPackageHandler_Ok() Assert.Equal(receivedBuffer.ToArray(), [5, 6, 0x13, 0x10]); // 关闭连接 - client.Close(); + await client.CloseAsync(); StopTcpServer(server); var handler = new DelimiterDataPackageHandler("\r\n"); @@ -608,7 +608,10 @@ class MockSendErrorHandler : DataPackageHandlerBase public override async ValueTask> SendAsync(ReadOnlyMemory data, CancellationToken token = default) { - Socket?.Close(); + if (Socket != null) + { + await Socket.CloseAsync(); + } await Task.Delay(10, token); return data; } From 7f514a3b37757127420376d2af502b6ae0dcdd3e Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 27 Jun 2025 09:01:44 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E7=B2=BE=E7=AE=80=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 | 7 +++++-- .../Services/TcpSocket/TcpSocketClientBase.cs | 4 ---- test/UnitTest/Services/TcpSocketFactoryTest.cs | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index 7459797fc03..e9fbfa539c7 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -26,6 +26,8 @@ sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : TcpSocketClientB public override void SetDataHandler(IDataPackageHandler handler) { + base.SetDataHandler(handler); + _dataPackageHandler = handler; } @@ -214,8 +216,10 @@ private async ValueTask ReceiveCoreAsync(TcpClient client, Memory buf return len; } - protected override ValueTask DisposeAsync(bool disposing) + protected override async ValueTask DisposeAsync(bool disposing) { + await base.DisposeAsync(disposing); + if (disposing) { LocalEndPoint = null; @@ -236,6 +240,5 @@ protected override ValueTask DisposeAsync(bool disposing) _client = null; } } - return ValueTask.CompletedTask; } } diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs index 5e17b5f4b6a..9ad52cfb74f 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs @@ -105,10 +105,6 @@ public virtual ValueTask CloseAsync() /// unmanaged resources. protected virtual ValueTask DisposeAsync(bool disposing) { - if (disposing) - { - LocalEndPoint = null; - } return ValueTask.CompletedTask; } diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index dcdcf21e4bd..a9f2e5349de 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -101,7 +101,7 @@ public async Task SendAsync_Error() // 测试未建立连接前调用 SendAsync 方法报异常逻辑 var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); var ex = await Assert.ThrowsAsync(async () => await client.SendAsync(data)); - Assert.Equal("TCP Socket is not connected 127.0.0.1:0", ex.Message); + Assert.NotNull(ex); } [Fact] From dc822e948c66da15f7d7149773abe062d4b52bfc Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 27 Jun 2025 09:06:32 +0800 Subject: [PATCH 6/6] =?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 --- .../Services/TcpSocket/DefaultTcpSocketClient.cs | 16 ++++------------ .../Services/TcpSocket/TcpSocketClientBase.cs | 7 ++++++- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index e9fbfa539c7..8b2839a8392 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -15,7 +15,6 @@ namespace BootstrapBlazor.Components; sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : TcpSocketClientBase { private TcpClient? _client; - private IDataPackageHandler? _dataPackageHandler; private CancellationTokenSource? _receiveCancellationTokenSource; private IPEndPoint? _remoteEndPoint; @@ -24,13 +23,6 @@ sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : TcpSocketClientB [NotNull] public ILogger? Logger { get; set; } - public override void SetDataHandler(IDataPackageHandler handler) - { - base.SetDataHandler(handler); - - _dataPackageHandler = handler; - } - public override async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) { var ret = false; @@ -99,9 +91,9 @@ public override async ValueTask SendAsync(ReadOnlyMemory data, Cance sendToken = CancellationTokenSource.CreateLinkedTokenSource(token, sendTokenSource.Token).Token; } - if (_dataPackageHandler != null) + if (DataPackageHandler != null) { - data = await _dataPackageHandler.SendAsync(data, sendToken); + data = await DataPackageHandler.SendAsync(data, sendToken); } await stream.WriteAsync(data, sendToken); @@ -192,9 +184,9 @@ private async ValueTask ReceiveCoreAsync(TcpClient client, Memory buf await ReceivedCallBack(buffer); } - if (_dataPackageHandler != null) + if (DataPackageHandler != null) { - await _dataPackageHandler.ReceiveAsync(buffer, receiveToken); + await DataPackageHandler.ReceiveAsync(buffer, receiveToken); } } } diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs index 9ad52cfb74f..0017109defe 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs @@ -56,12 +56,17 @@ public abstract class TcpSocketClientBase : ITcpSocketClient /// public int ReceiveTimeout { get; set; } + /// + /// Gets or sets the handler responsible for processing data packages. + /// + public IDataPackageHandler? DataPackageHandler { get; protected set; } + /// /// /// public virtual void SetDataHandler(IDataPackageHandler handler) { - + DataPackageHandler = handler; } ///