Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 12 additions & 44 deletions src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,24 @@
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<DefaultTcpSocketClient>? Logger { get; set; }

public int ReceiveBufferSize { get; set; } = 1024 * 64;

public bool IsAutoReceive { get; set; } = true;

public Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack { get; set; }

public int ConnectTimeout { get; set; }

public int SendTimeout { get; set; }

public int ReceiveTimeout { get; set; }

public void SetDataHandler(IDataPackageHandler handler)
{
_dataPackageHandler = handler;
}

public async ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken token = default)
public override async ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken token = default)
{
var ret = false;
try
{
// 释放资源
Close();
await CloseAsync();

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

public async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default)
public override async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default)
{
if (_client is not { Connected: true })
{
Expand All @@ -111,9 +91,9 @@ public async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationTo
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);
Expand All @@ -137,7 +117,7 @@ public async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationTo
return ret;
}

public async ValueTask<Memory<byte>> ReceiveAsync(CancellationToken token = default)
public override async ValueTask<Memory<byte>> ReceiveAsync(CancellationToken token = default)
{
if (_client == null || !_client.Connected)
{
Expand Down Expand Up @@ -204,9 +184,9 @@ private async ValueTask<int> ReceiveCoreAsync(TcpClient client, Memory<byte> buf
await ReceivedCallBack(buffer);
}

if (_dataPackageHandler != null)
if (DataPackageHandler != null)
{
await _dataPackageHandler.ReceiveAsync(buffer, receiveToken);
await DataPackageHandler.ReceiveAsync(buffer, receiveToken);
}
}
}
Expand All @@ -228,13 +208,10 @@ private async ValueTask<int> ReceiveCoreAsync(TcpClient client, Memory<byte> buf
return len;
}

public void Close()
protected override async ValueTask DisposeAsync(bool disposing)
{
Dispose(true);
}
await base.DisposeAsync(disposing);

private void Dispose(bool disposing)
{
if (disposing)
{
LocalEndPoint = null;
Expand All @@ -256,13 +233,4 @@ private void Dispose(bool disposing)
}
}
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ public ITcpSocketClient GetOrCreate(string name, Func<string, IPEndPoint> 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();
}
Expand All @@ -55,9 +55,9 @@ private void Dispose(bool disposing)
/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
public async ValueTask DisposeAsync()
{
Dispose(true);
await DisposeAsync(true);
GC.SuppressFinalize(this);
}
}
4 changes: 2 additions & 2 deletions src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// Represents a TCP socket for network communication.
/// </summary>
public interface ITcpSocketClient : IDisposable
public interface ITcpSocketClient : IAsyncDisposable
{
/// <summary>
/// Gets or sets the size, in bytes, of the receive buffer used for network operations.
Expand Down Expand Up @@ -110,5 +110,5 @@ public interface ITcpSocketClient : IDisposable
/// <remarks>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.</remarks>
void Close();
ValueTask CloseAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// ITcpSocketFactory Interface
/// </summary>
public interface ITcpSocketFactory : IDisposable
public interface ITcpSocketFactory : IAsyncDisposable
{
/// <summary>
/// Retrieves an existing TCP socket client by name or creates a new one using the specified factory function.
Expand Down
124 changes: 124 additions & 0 deletions src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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([email protected]) Website: https://www.blazor.zone

using System.Net;

namespace BootstrapBlazor.Components;

/// <summary>
/// Provides a base implementation for a TCP socket client, enabling connection, data transmission, and reception over
/// TCP.
/// </summary>
/// <remarks>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.</remarks>
public abstract class TcpSocketClientBase : ITcpSocketClient
{
/// <summary>
/// <inheritdoc/>
/// </summary>
public abstract bool IsConnected { get; }

/// <summary>
/// <inheritdoc/>
/// </summary>
public IPEndPoint? LocalEndPoint { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
public int ReceiveBufferSize { get; set; } = 1024 * 64;

/// <summary>
/// <inheritdoc/>
/// </summary>
public bool IsAutoReceive { get; set; } = true;

/// <summary>
/// <inheritdoc/>
/// </summary>
public Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
public int ConnectTimeout { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
public int SendTimeout { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
public int ReceiveTimeout { get; set; }

/// <summary>
/// Gets or sets the handler responsible for processing data packages.
/// </summary>
public IDataPackageHandler? DataPackageHandler { get; protected set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
public virtual void SetDataHandler(IDataPackageHandler handler)
{
DataPackageHandler = handler;
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="endPoint"></param>
/// <param name="token"></param>
/// <returns></returns>
public abstract ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken token = default);

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="data"></param>
/// <param name="token"></param>
/// <returns></returns>
public abstract ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default);

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public abstract ValueTask<Memory<byte>> ReceiveAsync(CancellationToken token = default);

/// <summary>
/// <inheritdoc/>
/// </summary>
public virtual ValueTask CloseAsync()
{
return DisposeAsync(true);
}

/// <summary>
/// Releases the resources used by the current instance of the class.
/// </summary>
/// <remarks>This method is called to free both managed and unmanaged resources. If the <paramref
/// name="disposing"/> parameter is <see langword="true"/>, the method releases managed resources in addition to
/// unmanaged resources. Override this method in a derived class to provide custom cleanup logic.</remarks>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only
/// unmanaged resources.</param>
protected virtual ValueTask DisposeAsync(bool disposing)
{
return ValueTask.CompletedTask;
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public async ValueTask DisposeAsync()
{
await DisposeAsync(true);
GC.SuppressFinalize(this);
}
}
23 changes: 13 additions & 10 deletions test/UnitTest/Services/TcpSocketFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -25,7 +25,7 @@ public void GetOrCreate_Ok()
var provider = sc.BuildServiceProvider();
var factory = provider.GetRequiredService<ITcpSocketFactory>();
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);
Expand All @@ -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]
Expand Down Expand Up @@ -101,7 +101,7 @@ public async Task SendAsync_Error()
// 测试未建立连接前调用 SendAsync 方法报异常逻辑
var data = new ReadOnlyMemory<byte>([1, 2, 3, 4, 5]);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await client.SendAsync(data));
Assert.Equal("TCP Socket is not connected 127.0.0.1:0", ex.Message);
Assert.NotNull(ex);
}

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

client.Close();
await client.CloseAsync();
client.IsAutoReceive = false;
var connected = await client.ConnectAsync("localhost", port);
Assert.True(connected);
Expand Down Expand Up @@ -342,7 +342,7 @@ public async Task FixLengthDataPackageHandler_Ok()
await Task.Delay(10);

// 关闭连接
client.Close();
await client.CloseAsync();
StopTcpServer(server);
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -608,7 +608,10 @@ class MockSendErrorHandler : DataPackageHandlerBase

public override async ValueTask<ReadOnlyMemory<byte>> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default)
{
Socket?.Close();
if (Socket != null)
{
await Socket.CloseAsync();
}
await Task.Delay(10, token);
return data;
}
Expand Down