Skip to content

Commit a8c8dc7

Browse files
authored
feat(TcpSocketClientBase): add TcpSocketClientBase class (#6320)
* refactor: 更改为异步销毁接口 * feat: 增加基类 * refactor: 更改 Close 为 CloseAsync * test: 更新单元测试 * refactor: 代码重构精简逻辑 * refactor: 精简代码逻辑
1 parent 45c9979 commit a8c8dc7

File tree

6 files changed

+156
-61
lines changed

6 files changed

+156
-61
lines changed

src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,44 +12,24 @@
1212
namespace BootstrapBlazor.Components;
1313

1414
[UnsupportedOSPlatform("browser")]
15-
sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : ITcpSocketClient
15+
sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : TcpSocketClientBase
1616
{
1717
private TcpClient? _client;
18-
private IDataPackageHandler? _dataPackageHandler;
1918
private CancellationTokenSource? _receiveCancellationTokenSource;
2019
private IPEndPoint? _remoteEndPoint;
2120

22-
public bool IsConnected => _client?.Connected ?? false;
23-
24-
public IPEndPoint? LocalEndPoint { get; set; }
21+
public override bool IsConnected => _client?.Connected ?? false;
2522

2623
[NotNull]
2724
public ILogger<DefaultTcpSocketClient>? Logger { get; set; }
2825

29-
public int ReceiveBufferSize { get; set; } = 1024 * 64;
30-
31-
public bool IsAutoReceive { get; set; } = true;
32-
33-
public Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack { get; set; }
34-
35-
public int ConnectTimeout { get; set; }
36-
37-
public int SendTimeout { get; set; }
38-
39-
public int ReceiveTimeout { get; set; }
40-
41-
public void SetDataHandler(IDataPackageHandler handler)
42-
{
43-
_dataPackageHandler = handler;
44-
}
45-
46-
public async ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken token = default)
26+
public override async ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken token = default)
4727
{
4828
var ret = false;
4929
try
5030
{
5131
// 释放资源
52-
Close();
32+
await CloseAsync();
5333

5434
// 创建新的 TcpClient 实例
5535
_client ??= new TcpClient(localEndPoint);
@@ -91,7 +71,7 @@ public async ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken
9171
return ret;
9272
}
9373

94-
public async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default)
74+
public override async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default)
9575
{
9676
if (_client is not { Connected: true })
9777
{
@@ -111,9 +91,9 @@ public async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationTo
11191
sendToken = CancellationTokenSource.CreateLinkedTokenSource(token, sendTokenSource.Token).Token;
11292
}
11393

114-
if (_dataPackageHandler != null)
94+
if (DataPackageHandler != null)
11595
{
116-
data = await _dataPackageHandler.SendAsync(data, sendToken);
96+
data = await DataPackageHandler.SendAsync(data, sendToken);
11797
}
11898

11999
await stream.WriteAsync(data, sendToken);
@@ -137,7 +117,7 @@ public async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationTo
137117
return ret;
138118
}
139119

140-
public async ValueTask<Memory<byte>> ReceiveAsync(CancellationToken token = default)
120+
public override async ValueTask<Memory<byte>> ReceiveAsync(CancellationToken token = default)
141121
{
142122
if (_client == null || !_client.Connected)
143123
{
@@ -204,9 +184,9 @@ private async ValueTask<int> ReceiveCoreAsync(TcpClient client, Memory<byte> buf
204184
await ReceivedCallBack(buffer);
205185
}
206186

207-
if (_dataPackageHandler != null)
187+
if (DataPackageHandler != null)
208188
{
209-
await _dataPackageHandler.ReceiveAsync(buffer, receiveToken);
189+
await DataPackageHandler.ReceiveAsync(buffer, receiveToken);
210190
}
211191
}
212192
}
@@ -228,13 +208,10 @@ private async ValueTask<int> ReceiveCoreAsync(TcpClient client, Memory<byte> buf
228208
return len;
229209
}
230210

231-
public void Close()
211+
protected override async ValueTask DisposeAsync(bool disposing)
232212
{
233-
Dispose(true);
234-
}
213+
await base.DisposeAsync(disposing);
235214

236-
private void Dispose(bool disposing)
237-
{
238215
if (disposing)
239216
{
240217
LocalEndPoint = null;
@@ -256,13 +233,4 @@ private void Dispose(bool disposing)
256233
}
257234
}
258235
}
259-
260-
/// <summary>
261-
/// <inheritdoc/>
262-
/// </summary>
263-
public void Dispose()
264-
{
265-
Dispose(true);
266-
GC.SuppressFinalize(this);
267-
}
268236
}

src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ public ITcpSocketClient GetOrCreate(string name, Func<string, IPEndPoint> valueF
3939
return client;
4040
}
4141

42-
private void Dispose(bool disposing)
42+
private async ValueTask DisposeAsync(bool disposing)
4343
{
4444
if (disposing)
4545
{
4646
// 释放托管资源
4747
foreach (var socket in _pool.Values)
4848
{
49-
socket.Dispose();
49+
await socket.DisposeAsync();
5050
}
5151
_pool.Clear();
5252
}
@@ -55,9 +55,9 @@ private void Dispose(bool disposing)
5555
/// <summary>
5656
/// <inheritdoc/>
5757
/// </summary>
58-
public void Dispose()
58+
public async ValueTask DisposeAsync()
5959
{
60-
Dispose(true);
60+
await DisposeAsync(true);
6161
GC.SuppressFinalize(this);
6262
}
6363
}

src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace BootstrapBlazor.Components;
1010
/// <summary>
1111
/// Represents a TCP socket for network communication.
1212
/// </summary>
13-
public interface ITcpSocketClient : IDisposable
13+
public interface ITcpSocketClient : IAsyncDisposable
1414
{
1515
/// <summary>
1616
/// Gets or sets the size, in bytes, of the receive buffer used for network operations.
@@ -110,5 +110,5 @@ public interface ITcpSocketClient : IDisposable
110110
/// <remarks>Once the connection or resource is closed, it cannot be reopened. Ensure that all necessary
111111
/// operations are completed before calling this method. This method is typically used to clean up resources when
112112
/// they are no longer needed.</remarks>
113-
void Close();
113+
ValueTask CloseAsync();
114114
}

src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace BootstrapBlazor.Components;
1010
/// <summary>
1111
/// ITcpSocketFactory Interface
1212
/// </summary>
13-
public interface ITcpSocketFactory : IDisposable
13+
public interface ITcpSocketFactory : IAsyncDisposable
1414
{
1515
/// <summary>
1616
/// Retrieves an existing TCP socket client by name or creates a new one using the specified factory function.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
using System.Net;
7+
8+
namespace BootstrapBlazor.Components;
9+
10+
/// <summary>
11+
/// Provides a base implementation for a TCP socket client, enabling connection, data transmission, and reception over
12+
/// TCP.
13+
/// </summary>
14+
/// <remarks>This abstract class serves as a foundation for implementing TCP socket clients. It provides methods
15+
/// for connecting to a remote endpoint, sending and receiving data, and managing connection state. Derived classes can
16+
/// extend or customize the behavior as needed.</remarks>
17+
public abstract class TcpSocketClientBase : ITcpSocketClient
18+
{
19+
/// <summary>
20+
/// <inheritdoc/>
21+
/// </summary>
22+
public abstract bool IsConnected { get; }
23+
24+
/// <summary>
25+
/// <inheritdoc/>
26+
/// </summary>
27+
public IPEndPoint? LocalEndPoint { get; set; }
28+
29+
/// <summary>
30+
/// <inheritdoc/>
31+
/// </summary>
32+
public int ReceiveBufferSize { get; set; } = 1024 * 64;
33+
34+
/// <summary>
35+
/// <inheritdoc/>
36+
/// </summary>
37+
public bool IsAutoReceive { get; set; } = true;
38+
39+
/// <summary>
40+
/// <inheritdoc/>
41+
/// </summary>
42+
public Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack { get; set; }
43+
44+
/// <summary>
45+
/// <inheritdoc/>
46+
/// </summary>
47+
public int ConnectTimeout { get; set; }
48+
49+
/// <summary>
50+
/// <inheritdoc/>
51+
/// </summary>
52+
public int SendTimeout { get; set; }
53+
54+
/// <summary>
55+
/// <inheritdoc/>
56+
/// </summary>
57+
public int ReceiveTimeout { get; set; }
58+
59+
/// <summary>
60+
/// Gets or sets the handler responsible for processing data packages.
61+
/// </summary>
62+
public IDataPackageHandler? DataPackageHandler { get; protected set; }
63+
64+
/// <summary>
65+
/// <inheritdoc/>
66+
/// </summary>
67+
public virtual void SetDataHandler(IDataPackageHandler handler)
68+
{
69+
DataPackageHandler = handler;
70+
}
71+
72+
/// <summary>
73+
/// <inheritdoc/>
74+
/// </summary>
75+
/// <param name="endPoint"></param>
76+
/// <param name="token"></param>
77+
/// <returns></returns>
78+
public abstract ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken token = default);
79+
80+
/// <summary>
81+
/// <inheritdoc/>
82+
/// </summary>
83+
/// <param name="data"></param>
84+
/// <param name="token"></param>
85+
/// <returns></returns>
86+
public abstract ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default);
87+
88+
/// <summary>
89+
/// <inheritdoc/>
90+
/// </summary>
91+
/// <param name="token"></param>
92+
/// <returns></returns>
93+
public abstract ValueTask<Memory<byte>> ReceiveAsync(CancellationToken token = default);
94+
95+
/// <summary>
96+
/// <inheritdoc/>
97+
/// </summary>
98+
public virtual ValueTask CloseAsync()
99+
{
100+
return DisposeAsync(true);
101+
}
102+
103+
/// <summary>
104+
/// Releases the resources used by the current instance of the class.
105+
/// </summary>
106+
/// <remarks>This method is called to free both managed and unmanaged resources. If the <paramref
107+
/// name="disposing"/> parameter is <see langword="true"/>, the method releases managed resources in addition to
108+
/// unmanaged resources. Override this method in a derived class to provide custom cleanup logic.</remarks>
109+
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only
110+
/// unmanaged resources.</param>
111+
protected virtual ValueTask DisposeAsync(bool disposing)
112+
{
113+
return ValueTask.CompletedTask;
114+
}
115+
116+
/// <summary>
117+
/// <inheritdoc/>
118+
/// </summary>
119+
public async ValueTask DisposeAsync()
120+
{
121+
await DisposeAsync(true);
122+
GC.SuppressFinalize(this);
123+
}
124+
}

test/UnitTest/Services/TcpSocketFactoryTest.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace UnitTest.Services;
1313
public class TcpSocketFactoryTest
1414
{
1515
[Fact]
16-
public void GetOrCreate_Ok()
16+
public async Task GetOrCreate_Ok()
1717
{
1818
// 测试 GetOrCreate 方法创建的 Client 销毁后继续 GetOrCreate 得到的对象是否可用
1919
var sc = new ServiceCollection();
@@ -25,7 +25,7 @@ public void GetOrCreate_Ok()
2525
var provider = sc.BuildServiceProvider();
2626
var factory = provider.GetRequiredService<ITcpSocketFactory>();
2727
var client1 = factory.GetOrCreate("demo", key => Utility.ConvertToIpEndPoint("localhost", 0));
28-
client1.Close();
28+
await client1.CloseAsync();
2929

3030
var client2 = factory.GetOrCreate("demo", key => Utility.ConvertToIpEndPoint("localhost", 0));
3131
Assert.Equal(client1, client2);
@@ -40,8 +40,8 @@ public void GetOrCreate_Ok()
4040
Assert.Equal(client4, client5);
4141
Assert.NotNull(client5);
4242

43-
client5.Dispose();
44-
factory.Dispose();
43+
await client5.DisposeAsync();
44+
await factory.DisposeAsync();
4545
}
4646

4747
[Fact]
@@ -101,7 +101,7 @@ public async Task SendAsync_Error()
101101
// 测试未建立连接前调用 SendAsync 方法报异常逻辑
102102
var data = new ReadOnlyMemory<byte>([1, 2, 3, 4, 5]);
103103
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await client.SendAsync(data));
104-
Assert.Equal("TCP Socket is not connected 127.0.0.1:0", ex.Message);
104+
Assert.NotNull(ex);
105105
}
106106

107107
[Fact]
@@ -211,7 +211,7 @@ public async Task ReceiveAsync_InvalidOperationException()
211211
ex = null;
212212
ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await client.ReceiveAsync());
213213

214-
client.Close();
214+
await client.CloseAsync();
215215
client.IsAutoReceive = false;
216216
var connected = await client.ConnectAsync("localhost", port);
217217
Assert.True(connected);
@@ -342,7 +342,7 @@ public async Task FixLengthDataPackageHandler_Ok()
342342
await Task.Delay(10);
343343

344344
// 关闭连接
345-
client.Close();
345+
await client.CloseAsync();
346346
StopTcpServer(server);
347347
}
348348

@@ -394,7 +394,7 @@ public async Task FixLengthDataPackageHandler_Sticky()
394394
Assert.Equal(receivedBuffer.ToArray(), [3, 2, 3, 4, 5, 6, 7]);
395395

396396
// 关闭连接
397-
client.Close();
397+
await client.CloseAsync();
398398
StopTcpServer(server);
399399
}
400400

@@ -441,7 +441,7 @@ public async Task DelimiterDataPackageHandler_Ok()
441441
Assert.Equal(receivedBuffer.ToArray(), [5, 6, 0x13, 0x10]);
442442

443443
// 关闭连接
444-
client.Close();
444+
await client.CloseAsync();
445445
StopTcpServer(server);
446446

447447
var handler = new DelimiterDataPackageHandler("\r\n");
@@ -608,7 +608,10 @@ class MockSendErrorHandler : DataPackageHandlerBase
608608

609609
public override async ValueTask<ReadOnlyMemory<byte>> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default)
610610
{
611-
Socket?.Close();
611+
if (Socket != null)
612+
{
613+
await Socket.CloseAsync();
614+
}
612615
await Task.Delay(10, token);
613616
return data;
614617
}

0 commit comments

Comments
 (0)