diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultSocketClientProvider.cs similarity index 94% rename from src/BootstrapBlazor/Services/TcpSocket/DefaultSocketClient.cs rename to src/BootstrapBlazor/Services/TcpSocket/DefaultSocketClientProvider.cs index 9b83c9197f0..df9da26960e 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultSocketClientProvider.cs @@ -13,7 +13,7 @@ namespace BootstrapBlazor.Components; /// TcpSocket 客户端默认实现 /// [UnsupportedOSPlatform("browser")] -class DefaultSocketClient(IPEndPoint localEndPoint) : ISocketClient +class DefaultSocketClientProvider : ISocketClientProvider { private TcpClient? _client; @@ -25,7 +25,7 @@ class DefaultSocketClient(IPEndPoint localEndPoint) : ISocketClient /// /// /// - public IPEndPoint LocalEndPoint { get; set; } = localEndPoint; + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); /// /// diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index 6ac0cc02d90..f8c0aa1c86e 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -3,32 +3,12 @@ // 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.Runtime.Versioning; namespace BootstrapBlazor.Components; [UnsupportedOSPlatform("browser")] -sealed class DefaultTcpSocketClient : TcpSocketClientBase +sealed class DefaultTcpSocketClient(SocketClientOptions options) : TcpSocketClientBase(options) { - public DefaultTcpSocketClient(SocketClientOptions options) - { - ReceiveBufferSize = Math.Max(1024, options.ReceiveBufferSize); - IsAutoReceive = options.IsAutoReceive; - ConnectTimeout = options.ConnectTimeout; - SendTimeout = options.SendTimeout; - ReceiveTimeout = options.ReceiveTimeout; - LocalEndPoint = options.LocalEndPoint; - } - /// - /// - /// - /// - /// - /// - protected override DefaultSocketClient CreateSocketClient(IPEndPoint localEndPoint) - { - return new DefaultSocketClient(localEndPoint); - } } diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs index 49037b345f8..55f8815c58c 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.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 Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Runtime.Versioning; @@ -23,7 +21,7 @@ public ITcpSocketClient GetOrCreate(string name, Action val valueFactory(options); var client = new DefaultTcpSocketClient(options) { - Logger = provider.GetService>() + ServiceProvider = provider, }; return client; }); diff --git a/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs b/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs index c88529509fa..dde3bc7aa8f 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs @@ -22,9 +22,11 @@ public static class TcpSocketExtensions /// public static IServiceCollection AddBootstrapBlazorTcpSocketFactory(this IServiceCollection services) { - // 添加 ITcpSocket 实现 - services.TryAddSingleton(); + // 添加 ITcpSocketFactory 服务 + services.AddSingleton(); + // 增加 ISocketClientProvider 服务 + services.TryAddTransient(); return services; } } diff --git a/src/BootstrapBlazor/Services/TcpSocket/ISocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ISocketClientProvider.cs similarity index 92% rename from src/BootstrapBlazor/Services/TcpSocket/ISocketClient.cs rename to src/BootstrapBlazor/Services/TcpSocket/ISocketClientProvider.cs index 27962b3d6c4..6c3bd9e862b 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ISocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ISocketClientProvider.cs @@ -15,7 +15,7 @@ namespace BootstrapBlazor.Components; /// establishing connections, transmitting data, and receiving data asynchronously. Implementations of this interface /// should ensure proper resource management, including closing connections and releasing resources when no longer /// needed. -public interface ISocketClient +public interface ISocketClientProvider { /// /// Gets a value indicating whether the connection is currently active. @@ -25,10 +25,7 @@ public interface ISocketClient /// /// 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; } + IPEndPoint LocalEndPoint { get; set; } /// /// Establishes an asynchronous connection to the specified endpoint. diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs index e199d1744f0..1d38e26dff3 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs @@ -12,40 +12,15 @@ namespace BootstrapBlazor.Components; /// public interface ITcpSocketClient : IAsyncDisposable { - /// - /// 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. Default is false. /// bool IsConnected { get; } /// - /// Gets or sets a value indicating whether automatic receiving data is enabled. Default is true. - /// - bool IsAutoReceive { get; set; } - - /// - /// Gets or sets the timeout duration, in milliseconds, for establishing a connection. - /// - int ConnectTimeout { get; set; } - - /// - /// Gets or sets the duration, in milliseconds, to wait for a send operation to complete before timing out. - /// - /// If the send operation does not complete within the specified timeout period, an exception may - /// be thrown. - int SendTimeout { get; set; } - - /// - /// Gets or sets the amount of time, in milliseconds, that the receiver will wait for a response before timing out. + /// Gets or sets the configuration options for the socket client. /// - /// Use this property to configure the maximum wait time for receiving a response. Setting an - /// appropriate timeout can help prevent indefinite blocking in scenarios where responses may be delayed or - /// unavailable. - int ReceiveTimeout { get; set; } + SocketClientOptions Options { get; } /// /// Gets the local network endpoint that the socket is bound to. diff --git a/src/BootstrapBlazor/Services/TcpSocket/SocketClientOptions.cs b/src/BootstrapBlazor/Services/TcpSocket/SocketClientOptions.cs index 95d7fecf6f6..72904adfd72 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/SocketClientOptions.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/SocketClientOptions.cs @@ -46,8 +46,8 @@ public class SocketClientOptions public int ReceiveTimeout { get; set; } /// - /// Gets or sets the local endpoint for the socket client. Default value is + /// Gets or sets the local endpoint for the socket client. Default value is /// /// This property specifies the local network endpoint that the socket client will bind to when establishing a connection. - public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Loopback, 0); + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); } diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs index 9bd992bfc02..05aff55447b 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.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.DependencyInjection; using Microsoft.Extensions.Logging; using System.Buffers; using System.Net; @@ -10,63 +11,55 @@ namespace BootstrapBlazor.Components; /// -/// Provides a base implementation for a TCP socket client, enabling connection, data transmission, and reception over -/// TCP. +/// Provides a base implementation for TCP socket clients, enabling connection management, data transmission, and +/// reception over TCP sockets. This class is designed to be extended by specific implementations of TCP socket +/// clients. /// -/// 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 where TSocketClient : class, ISocketClient +/// The class offers core functionality for managing TCP socket +/// connections, including connecting to remote endpoints, sending and receiving data, and handling data packages. +/// Derived classes can extend or override its behavior to implement specific client logic. Key features include: - +/// Connection management with support for timeouts and cancellation tokens. - Data transmission and reception with +/// optional data package handling. - Logging capabilities for tracking events and errors. - Dependency injection +/// support via . This class is abstract and cannot be instantiated directly. Use a +/// derived class to implement specific functionality. +/// +public abstract class TcpSocketClientBase(SocketClientOptions options) : ITcpSocketClient { /// - /// Gets or sets the underlying socket client used for network communication. + /// Gets or sets the socket client provider used for managing socket connections. /// - protected TSocketClient? Client { get; set; } + protected ISocketClientProvider? SocketClientProvider { get; set; } - /// - /// - /// - public ILogger? Logger { get; set; } - - /// - /// - /// - public bool IsConnected => Client?.IsConnected ?? false; - - /// - /// - /// - public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Loopback, 0); /// - /// + /// Gets or sets the logger instance used for logging messages and events. /// - public int ReceiveBufferSize { get; set; } = 1024 * 64; + protected ILogger? Logger { get; set; } /// - /// + /// Gets or sets the service provider used to resolve dependencies. /// - public bool IsAutoReceive { get; set; } = true; + public IServiceProvider? ServiceProvider { get; set; } /// /// /// - public Func, ValueTask>? ReceivedCallBack { get; set; } + public SocketClientOptions Options => options; /// /// /// - public int ConnectTimeout { get; set; } + public bool IsConnected => SocketClientProvider?.IsConnected ?? false; /// /// /// - public int SendTimeout { get; set; } + public IPEndPoint LocalEndPoint => SocketClientProvider?.LocalEndPoint ?? new IPEndPoint(IPAddress.Any, 0); /// /// /// - public int ReceiveTimeout { get; set; } + public Func, ValueTask>? ReceivedCallBack { get; set; } /// /// Gets or sets the handler responsible for processing data packages. @@ -77,13 +70,6 @@ public abstract class TcpSocketClientBase : ITcpSocketClient wher private IPEndPoint? _localEndPoint; private CancellationTokenSource? _receiveCancellationTokenSource; - /// - /// Creates and initializes a new instance of the socket client for the specified endpoint. - /// - /// The network endpoint to which the socket client will connect. Cannot be null. - /// An instance of configured for the specified endpoint. - protected abstract TSocketClient CreateSocketClient(IPEndPoint localEndPoint); - /// /// /// @@ -101,36 +87,39 @@ public virtual void SetDataHandler(IDataPackageHandler handler) public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) { var ret = false; + SocketClientProvider = ServiceProvider?.GetRequiredService() + ?? throw new InvalidOperationException("SocketClientProvider is not registered in the service provider."); + try { // 释放资源 await CloseAsync(); // 创建新的 TcpClient 实例 - Client ??= CreateSocketClient(LocalEndPoint); - _localEndPoint = LocalEndPoint; + SocketClientProvider.LocalEndPoint = Options.LocalEndPoint; + + _localEndPoint = Options.LocalEndPoint; _remoteEndPoint = null; var connectionToken = token; - if (ConnectTimeout > 0) + if (Options.ConnectTimeout > 0) { // 设置连接超时时间 - var connectTokenSource = new CancellationTokenSource(ConnectTimeout); + var connectTokenSource = new CancellationTokenSource(options.ConnectTimeout); connectionToken = CancellationTokenSource.CreateLinkedTokenSource(token, connectTokenSource.Token).Token; } - await Client.ConnectAsync(endPoint, connectionToken); + ret = await SocketClientProvider.ConnectAsync(endPoint, connectionToken); - if (Client.IsConnected) + if (ret) { - _localEndPoint = Client.LocalEndPoint; + _localEndPoint = SocketClientProvider.LocalEndPoint; _remoteEndPoint = endPoint; - if (IsAutoReceive) + if (options.IsAutoReceive) { _ = Task.Run(AutoReceiveAsync, token); } } - ret = Client.IsConnected; } catch (OperationCanceledException ex) { @@ -154,7 +143,7 @@ public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken /// public virtual async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) { - if (Client is not { IsConnected: true }) + if (SocketClientProvider is not { IsConnected: true }) { throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); } @@ -163,10 +152,10 @@ public virtual async ValueTask SendAsync(ReadOnlyMemory data, Cancel try { var sendToken = token; - if (SendTimeout > 0) + if (options.SendTimeout > 0) { // 设置发送超时时间 - var sendTokenSource = new CancellationTokenSource(SendTimeout); + var sendTokenSource = new CancellationTokenSource(options.SendTimeout); sendToken = CancellationTokenSource.CreateLinkedTokenSource(token, sendTokenSource.Token).Token; } @@ -175,7 +164,7 @@ public virtual async ValueTask SendAsync(ReadOnlyMemory data, Cancel data = await DataPackageHandler.SendAsync(data, sendToken); } - ret = await Client.SendAsync(data, sendToken); + ret = await SocketClientProvider.SendAsync(data, sendToken); } catch (OperationCanceledException ex) { @@ -197,19 +186,19 @@ public virtual async ValueTask SendAsync(ReadOnlyMemory data, Cancel /// public virtual async ValueTask> ReceiveAsync(CancellationToken token = default) { - if (Client is not { IsConnected: true }) + if (SocketClientProvider is not { IsConnected: true }) { throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); } - if (IsAutoReceive) + if (options.IsAutoReceive) { throw new InvalidOperationException("Cannot call ReceiveAsync when IsAutoReceive is true. Use the auto-receive mechanism instead."); } - using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); + using var block = MemoryPool.Shared.Rent(options.ReceiveBufferSize); var buffer = block.Memory; - var len = await ReceiveCoreAsync(Client, buffer, token); + var len = await ReceiveCoreAsync(SocketClientProvider, buffer, token); return buffer[..len]; } @@ -218,14 +207,14 @@ private async ValueTask AutoReceiveAsync() _receiveCancellationTokenSource ??= new(); while (_receiveCancellationTokenSource is { IsCancellationRequested: false }) { - if (Client is not { IsConnected: true }) + if (SocketClientProvider is not { IsConnected: true }) { throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); } - using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); + using var block = MemoryPool.Shared.Rent(options.ReceiveBufferSize); var buffer = block.Memory; - var len = await ReceiveCoreAsync(Client, buffer, _receiveCancellationTokenSource.Token); + var len = await ReceiveCoreAsync(SocketClientProvider, buffer, _receiveCancellationTokenSource.Token); if (len == 0) { break; @@ -233,16 +222,16 @@ private async ValueTask AutoReceiveAsync() } } - private async ValueTask ReceiveCoreAsync(ISocketClient client, Memory buffer, CancellationToken token) + private async ValueTask ReceiveCoreAsync(ISocketClientProvider client, Memory buffer, CancellationToken token) { var len = 0; try { var receiveToken = token; - if (ReceiveTimeout > 0) + if (options.ReceiveTimeout > 0) { // 设置接收超时时间 - var receiveTokenSource = new CancellationTokenSource(ReceiveTimeout); + var receiveTokenSource = new CancellationTokenSource(options.ReceiveTimeout); receiveToken = CancellationTokenSource.CreateLinkedTokenSource(receiveToken, receiveTokenSource.Token).Token; } @@ -286,6 +275,7 @@ private async ValueTask ReceiveCoreAsync(ISocketClient client, Memory /// protected void Log(LogLevel logLevel, Exception? ex, string? message) { + Logger ??= ServiceProvider?.GetRequiredService>(); Logger?.Log(logLevel, ex, "{Message}", message); } @@ -317,9 +307,9 @@ protected virtual async ValueTask DisposeAsync(bool disposing) _receiveCancellationTokenSource = null; } - if (Client != null) + if (SocketClientProvider != null) { - await Client.CloseAsync(); + await SocketClientProvider.CloseAsync(); } } } diff --git a/src/BootstrapBlazor/Utils/Utility.cs b/src/BootstrapBlazor/Utils/Utility.cs index 2c9ff091d20..c2be34a3c4d 100644 --- a/src/BootstrapBlazor/Utils/Utility.cs +++ b/src/BootstrapBlazor/Utils/Utility.cs @@ -939,7 +939,7 @@ public static IPAddress ConvertToIPAddress(string ipString) [ExcludeFromCodeCoverage] [UnsupportedOSPlatform("browser")] - private static IPAddress IPAddressByHostName => Dns.GetHostAddresses(Dns.GetHostName(), AddressFamily.InterNetwork).FirstOrDefault() ?? IPAddress.Loopback; + private static IPAddress IPAddressByHostName => Dns.GetHostAddresses(Dns.GetHostName(), AddressFamily.InterNetwork).FirstOrDefault() ?? IPAddress.Any; /// /// Converts a string representation of an IP address and a port number into an instance. diff --git a/test/UnitTest/Services/DefaultSocketClientProviderTest.cs b/test/UnitTest/Services/DefaultSocketClientProviderTest.cs new file mode 100644 index 00000000000..8e77c51a936 --- /dev/null +++ b/test/UnitTest/Services/DefaultSocketClientProviderTest.cs @@ -0,0 +1,48 @@ +// 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 UnitTest.Services; + +public class DefaultSocketClientProviderTest +{ + [Fact] + public async Task DefaultSocketClient_Ok() + { + var sc = new ServiceCollection(); + sc.AddBootstrapBlazorTcpSocketFactory(); + var provider = sc.BuildServiceProvider(); + var clientProvider = provider.GetRequiredService(); + + // 未建立连接时 IsConnected 应为 false + Assert.False(clientProvider.IsConnected); + + // 未建立连接直接调用 ReceiveAsync 方法 + var buffer = new byte[1024]; + var len = await clientProvider.ReceiveAsync(buffer); + Assert.Equal(0, len); + } + + [Fact] + public void SocketClientOptions_Ok() + { + var options = new SocketClientOptions + { + ReceiveBufferSize = 1024 * 64, + IsAutoReceive = true, + ConnectTimeout = 1000, + SendTimeout = 500, + ReceiveTimeout = 500, + LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0) + }; + Assert.Equal(1024 * 64, options.ReceiveBufferSize); + Assert.True(options.IsAutoReceive); + Assert.Equal(1000, options.ConnectTimeout); + Assert.Equal(500, options.SendTimeout); + Assert.Equal(500, options.ReceiveTimeout); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 0), options.LocalEndPoint); + } +} diff --git a/test/UnitTest/Services/DefaultSocketClientTest.cs b/test/UnitTest/Services/DefaultSocketClientTest.cs deleted file mode 100644 index 4fcdf0b5c96..00000000000 --- a/test/UnitTest/Services/DefaultSocketClientTest.cs +++ /dev/null @@ -1,231 +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 - -using Microsoft.Extensions.Logging; -using System.Net; -using System.Net.Sockets; -using System.Reflection; - -namespace UnitTest.Services; - -public class DefaultSocketClientTest -{ - [Fact] - public void Logger_Null() - { - // 测试 Logger 为 null 的情况 - var client = CreateClient(); - var baseType = client.GetType().BaseType; - Assert.NotNull(baseType); - - // 获取 Logger 字段设置为 null 测试 Log 不会抛出异常 - var propertyInfo = baseType.GetProperty("Logger", BindingFlags.Public | BindingFlags.Instance); - Assert.NotNull(propertyInfo); - - propertyInfo.SetValue(client, null); - - var methodInfo = baseType.GetMethod("Log", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(methodInfo); - methodInfo.Invoke(client, [LogLevel.Information, null!, "Test log message"]); - } - - [Fact] - public async Task DefaultSocketClient_Ok() - { - var port = 8894; - var server = StartTcpServer(port, MockDelimiterPackageAsync); - var client = CreateClient(); - - // 获得 Client 泛型属性 - var baseType = client.GetType().BaseType; - Assert.NotNull(baseType); - - // 建立连接 - var connect = await client.ConnectAsync("localhost", port); - Assert.True(connect); - - var propertyInfo = baseType.GetProperty("Client", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(propertyInfo); - var instance = propertyInfo.GetValue(client); - Assert.NotNull(instance); - - ISocketClient socketClient = (ISocketClient)instance; - Assert.NotNull(socketClient); - Assert.True(socketClient.IsConnected); - - await socketClient.CloseAsync(); - Assert.False(socketClient.IsConnected); - - var buffer = new byte[10]; - var len = await socketClient.ReceiveAsync(buffer); - Assert.Equal(0, len); - } - - [Fact] - public void SocketClientOptions_Ok() - { - var options = new SocketClientOptions - { - ReceiveBufferSize = 1024 * 64, - IsAutoReceive = true, - ConnectTimeout = 1000, - SendTimeout = 500, - ReceiveTimeout = 500, - LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0) - }; - Assert.Equal(1024 * 64, options.ReceiveBufferSize); - Assert.True(options.IsAutoReceive); - Assert.Equal(1000, options.ConnectTimeout); - Assert.Equal(500, options.SendTimeout); - Assert.Equal(500, options.ReceiveTimeout); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 0), options.LocalEndPoint); - } - - private static TcpListener StartTcpServer(int port, Func handler) - { - var server = new TcpListener(IPAddress.Loopback, port); - server.Start(); - Task.Run(() => AcceptClientsAsync(server, handler)); - return server; - } - - private static async Task AcceptClientsAsync(TcpListener server, Func handler) - { - while (true) - { - var client = await server.AcceptTcpClientAsync(); - _ = Task.Run(() => handler(client)); - } - } - - private static async Task MockDelimiterPackageAsync(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 ReadOnlyMemory(buffer, 0, len); - await stream.WriteAsync(block, CancellationToken.None); - - await Task.Delay(20); - - // 模拟拆包发送第二段数据 - await stream.WriteAsync(new byte[] { 0x13, 0x10, 0x5, 0x6, 0x13, 0x10 }, CancellationToken.None); - } - } - - private static ITcpSocketClient CreateClient() - { - 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("test", op => op.LocalEndPoint = Utility.ConvertToIpEndPoint("localhost", 0)); - return client; - } - - 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) - { - - } - } - - class MockSendErrorHandler : DataPackageHandlerBase - { - public ITcpSocketClient? Socket { get; set; } - - public override ValueTask> SendAsync(ReadOnlyMemory data, CancellationToken token = default) - { - throw new Exception("Mock send failed"); - } - } - - class MockSendCancelHandler : DataPackageHandlerBase - { - public ITcpSocketClient? Socket { get; set; } - - public override async ValueTask> SendAsync(ReadOnlyMemory data, CancellationToken token = default) - { - if (Socket != null) - { - await Socket.CloseAsync(); - } - await Task.Delay(10, token); - return data; - } - } - - class MockReceiveErrorHandler : DataPackageHandlerBase - { - public override ValueTask> SendAsync(ReadOnlyMemory data, CancellationToken token = default) - { - return ValueTask.FromResult(data); - } - - public override async ValueTask ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default) - { - await base.ReceiveAsync(data, token); - - // 模拟接收数据时报错 - throw new InvalidOperationException("Test Error"); - } - } - - class MockSendTimeoutHandler : DataPackageHandlerBase - { - public override async ValueTask> SendAsync(ReadOnlyMemory data, CancellationToken token = default) - { - // 模拟发送超时 - await Task.Delay(200, token); - return data; - } - } - - class MockReceiveTimeoutHandler : DataPackageHandlerBase - { - public override async ValueTask ReceiveAsync(ReadOnlyMemory data, CancellationToken token = default) - { - // 模拟接收超时 - await Task.Delay(200, token); - await base.ReceiveAsync(data, token); - } - } -} diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index e9d4b1de43a..9fdcffeee53 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -49,7 +49,7 @@ public async Task GetOrCreate_Ok() public async Task ConnectAsync_Timeout() { var client = CreateClient(); - client.ConnectTimeout = 100; + client.Options.ConnectTimeout = 1; var connect = await client.ConnectAsync("localhost", 9999); Assert.False(connect); @@ -77,6 +77,26 @@ public async Task ConnectAsync_Failed() Assert.False(connect); } + [Fact] + public async Task ConnectAsync_Error() + { + var client = CreateClient(); + + // 反射设置 SocketClientProvider 为空 + var propertyInfo = client.GetType().GetProperty("ServiceProvider", BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(propertyInfo); + propertyInfo.SetValue(client, null); + + // 测试 ConnectAsync 方法连接失败 + var ex = await Assert.ThrowsAsync(async () => await client.ConnectAsync("localhost", 9999)); + Assert.NotNull(ex); + + // 反射测试 Log 方法 + var methodInfo = client.GetType().GetMethod("Log", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(methodInfo); + methodInfo.Invoke(client, [LogLevel.Error, null!, "Test error log"]); + } + [Fact] public async Task Send_Timeout() { @@ -84,7 +104,7 @@ public async Task Send_Timeout() var server = StartTcpServer(port, MockSplitPackageAsync); var client = CreateClient(); - client.SendTimeout = 100; + client.Options.SendTimeout = 100; client.SetDataHandler(new MockSendTimeoutHandler()); await client.ConnectAsync("localhost", port); @@ -166,7 +186,7 @@ public async Task ReceiveAsync_Timeout() var server = StartTcpServer(port, MockSplitPackageAsync); var client = CreateClient(); - client.ReceiveTimeout = 100; + client.Options.ReceiveTimeout = 100; client.SetDataHandler(new MockReceiveTimeoutHandler()); await client.ConnectAsync("localhost", port); @@ -214,7 +234,7 @@ public async Task ReceiveAsync_InvalidOperationException() var port = 8893; var server = StartTcpServer(port, MockSplitPackageAsync); - client.IsAutoReceive = true; + client.Options.IsAutoReceive = true; var connected = await client.ConnectAsync("localhost", port); Assert.True(connected); @@ -229,7 +249,7 @@ public async Task ReceiveAsync_Ok() var server = StartTcpServer(port, MockSplitPackageAsync); var client = CreateClient(); - client.IsAutoReceive = false; + client.Options.IsAutoReceive = false; var connected = await client.ConnectAsync("localhost", port); Assert.True(connected); @@ -260,10 +280,10 @@ public async Task ReceiveAsync_Error() var port = 8882; var server = StartTcpServer(port, MockSplitPackageAsync); - Assert.Equal(1024 * 64, client.ReceiveBufferSize); + Assert.Equal(1024 * 64, client.Options.ReceiveBufferSize); - client.ReceiveBufferSize = 1024 * 20; - Assert.Equal(1024 * 20, client.ReceiveBufferSize); + client.Options.ReceiveBufferSize = 1024 * 20; + Assert.Equal(1024 * 20, client.Options.ReceiveBufferSize); client.SetDataHandler(new MockReceiveErrorHandler());