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;
+}
diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs
new file mode 100644
index 00000000000..cba5c6e3156
--- /dev/null
+++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/DataPackageHandlerBase.cs
@@ -0,0 +1,85 @@
+// 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
+{
+ private Memory _lastReceiveBuffer = Memory.Empty;
+
+ ///
+ /// 当接收数据处理完成后,回调该函数执行接收
+ ///
+ 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);
+ }
+
+ ///
+ /// Processes the received data asynchronously.
+ ///
+ /// 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.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 SlicePackage(Memory buffer, int length)
+ {
+ _lastReceiveBuffer = buffer[length..].ToArray().AsMemory();
+ }
+
+ ///
+ /// 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;
+ }
+
+ // 计算缓存区长度
+ Memory merged = new byte[_lastReceiveBuffer.Length + buffer.Length];
+ _lastReceiveBuffer.CopyTo(merged);
+ buffer.CopyTo(merged[_lastReceiveBuffer.Length..]);
+
+ // Clear the sticky buffer
+ _lastReceiveBuffer = Memory.Empty;
+ return merged;
+ }
+}
diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs
new file mode 100644
index 00000000000..3efbe4dee7b
--- /dev/null
+++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/FixLengthDataPackageHandler.cs
@@ -0,0 +1,55 @@
+// 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;
+
+///
+/// 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)
+ {
+ // 处理上次粘包数据
+ data = ConcatBuffer(data);
+
+ // 拷贝数据
+ var len = length - _receivedLength;
+ var segment = data.Length > len ? data[..len] : data;
+ segment.CopyTo(_data[_receivedLength..]);
+
+ if (data.Length > len)
+ {
+ SlicePackage(data, data.Length - len);
+ }
+
+ // 更新已接收长度
+ _receivedLength += segment.Length;
+
+ // 如果已接收长度等于总长度则触发回调
+ if (_receivedLength == length)
+ {
+ // 重置已接收长度
+ _receivedLength = 0;
+ if (ReceivedCallBack != null)
+ {
+ await ReceivedCallBack(_data);
+ }
+ }
+ }
+}
diff --git a/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs
new file mode 100644
index 00000000000..89c9f13d0be
--- /dev/null
+++ b/src/BootstrapBlazor/Services/TcpSocket/DataPackage/IDataPackageHandler.cs
@@ -0,0 +1,41 @@
+// 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 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.
+ ///
+ /// 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 from a source and writes it into the provided memory buffer.
+ ///
+ /// 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);
+}
diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs
new file mode 100644
index 00000000000..3701ac82f18
--- /dev/null
+++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs
@@ -0,0 +1,192 @@
+// 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.Buffers;
+using System.Net;
+using System.Net.Sockets;
+using System.Runtime.Versioning;
+
+namespace BootstrapBlazor.Components;
+
+[UnsupportedOSPlatform("browser")]
+class DefaultTcpSocketClient : ITcpSocketClient
+{
+ private TcpClient? _client;
+ private IDataPackageHandler? _dataPackageHandler;
+ private CancellationTokenSource? _receiveCancellationTokenSource;
+ private IPEndPoint? _remoteEndPoint;
+
+ public bool IsConnected => _client?.Connected ?? false;
+
+ public IPEndPoint LocalEndPoint { get; set; }
+
+ [NotNull]
+ 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);
+ }
+
+ private static IPAddress GetIPAddress(string host) => host.Equals("localhost", StringComparison.OrdinalIgnoreCase)
+ ? 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)
+ {
+ _dataPackageHandler = handler;
+ }
+
+ public Task ConnectAsync(string host, int port, CancellationToken token = default)
+ {
+ var endPoint = new IPEndPoint(GetIPAddress(host), port);
+ return ConnectAsync(endPoint, token);
+ }
+
+ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken token = default)
+ {
+ var ret = false;
+ try
+ {
+ // 释放资源
+ Close();
+
+ // 创建新的 TcpClient 实例
+ _client ??= new TcpClient(LocalEndPoint);
+ await _client.ConnectAsync(endPoint, token);
+
+ // 开始接收数据
+ _ = Task.Run(ReceiveAsync, token);
+
+ LocalEndPoint = (IPEndPoint)_client.Client.LocalEndPoint!;
+ _remoteEndPoint = endPoint;
+ ret = true;
+ }
+ catch (OperationCanceledException ex)
+ {
+ Logger.LogWarning(ex, "TCP Socket connect operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "TCP Socket connection failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint);
+ }
+ return ret;
+ }
+
+ public async Task SendAsync(Memory data, CancellationToken token = default)
+ {
+ if (_client is not { Connected: true })
+ {
+ throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}");
+ }
+
+ var ret = false;
+ try
+ {
+ if (_dataPackageHandler != null)
+ {
+ data = await _dataPackageHandler.SendAsync(data);
+ }
+ var stream = _client.GetStream();
+ await stream.WriteAsync(data, token);
+ ret = true;
+ }
+ catch (OperationCanceledException ex)
+ {
+ 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);
+ }
+ return ret;
+ }
+
+ private async Task ReceiveAsync()
+ {
+ _receiveCancellationTokenSource ??= new();
+ while (_receiveCancellationTokenSource is { IsCancellationRequested: false })
+ {
+ if (_client is not { Connected: true })
+ {
+ throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}");
+ }
+
+ 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)
+ {
+ // 远端主机关闭链路
+ Logger.LogInformation("TCP Socket {LocalEndPoint} received 0 data closed by {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint);
+ break;
+ }
+ else
+ {
+ buffer = buffer[..len];
+
+ if (_dataPackageHandler != null)
+ {
+ await _dataPackageHandler.ReceiveAsync(buffer);
+ }
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ 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);
+ }
+ }
+ }
+
+ public void Close()
+ {
+ Dispose(true);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _remoteEndPoint = null;
+
+ // 取消接收数据的任务
+ if (_receiveCancellationTokenSource is not null)
+ {
+ _receiveCancellationTokenSource.Cancel();
+ _receiveCancellationTokenSource.Dispose();
+ _receiveCancellationTokenSource = null;
+ }
+
+ // 释放 TcpClient 资源
+ if (_client != null)
+ {
+ _client.Close();
+ _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..8f52e380cd6
--- /dev/null
+++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketFactory.cs
@@ -0,0 +1,61 @@
+// 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.Logging;
+using System.Collections.Concurrent;
+using System.Runtime.Versioning;
+
+namespace BootstrapBlazor.Components;
+
+[UnsupportedOSPlatform("browser")]
+class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory
+{
+ private readonly ConcurrentDictionary _pool = new();
+
+ public ITcpSocketClient GetOrCreate(string host, int port = 0)
+ {
+ return _pool.GetOrAdd($"{host}:{port}", key =>
+ {
+ var client = new DefaultTcpSocketClient(host, port)
+ {
+ Logger = provider.GetService>()
+ };
+ return client;
+ });
+ }
+
+ 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)
+ {
+ // 释放托管资源
+ 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/Extensions/TcpSocketExtensions.cs b/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs
new file mode 100644
index 00000000000..81fde060e97
--- /dev/null
+++ b/src/BootstrapBlazor/Services/TcpSocket/Extensions/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 AddBootstrapBlazorTcpSocketFactory(this IServiceCollection services)
+ {
+ // 添加 ITcpSocket 实现
+ services.TryAddSingleton();
+
+ return services;
+ }
+}
diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs
new file mode 100644
index 00000000000..3c89a34b1db
--- /dev/null
+++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs
@@ -0,0 +1,81 @@
+// 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;
+
+///
+/// Represents a TCP socket for network communication.
+///
+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.
+ ///
+ 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; }
+
+ ///
+ /// Configures the data handler to process incoming data packages.
+ ///
+ /// The handler responsible for processing data packages. Cannot be null.
+ void SetDataHandler(IDataPackageHandler handler);
+
+ ///
+ /// Establishes an asynchronous connection to the specified host and port.
+ ///
+ /// 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);
+
+ ///
+ /// 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.
+ ///
+ /// 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(Memory data, 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();
+}
diff --git a/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs
new file mode 100644
index 00000000000..b72069e2f43
--- /dev/null
+++ b/src/BootstrapBlazor/Services/TcpSocket/ITcpSocketFactory.cs
@@ -0,0 +1,31 @@
+// 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.
+ /// An instance representing the TCP socket for the specified host and port.
+ 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/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!;
+ }
+}
diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs
new file mode 100644
index 00000000000..cf3fe492878
--- /dev/null
+++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs
@@ -0,0 +1,420 @@
+// 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;
+
+namespace UnitTest.Services;
+
+public class TcpSocketFactoryTest
+{
+ [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);
+
+ 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);
+ Assert.NotNull(client5);
+
+ client5.Dispose();
+
+ factory.Dispose();
+ }
+
+ [Fact]
+ public async Task ConnectAsync_Cancel()
+ {
+ var client = CreateClient();
+
+ // 测试 ConnectAsync 方法连接取消逻辑
+ var cst = new CancellationTokenSource();
+ cst.Cancel();
+ var connect = await client.ConnectAsync("localhost", 9999, cst.Token);
+ Assert.False(connect);
+ }
+
+ [Fact]
+ public async Task ConnectAsync_Failed()
+ {
+ var client = CreateClient();
+
+ // 测试 ConnectAsync 方法连接失败
+ var connect = await client.ConnectAsync("localhost", 9999);
+ 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();
+ Assert.False(client.IsConnected);
+
+ // 连接 TCP Server
+ 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 MockSendErrorHandler()
+ {
+ 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 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);
+
+ // 关闭连接
+ 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()
+ {
+ var port = 8888;
+ var server = StartTcpServer(port, MockSplitPackageAsync);
+ var client = CreateClient();
+
+ // 测试 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_Sticky()
+ {
+ var port = 8899;
+ var server = StartTcpServer(port, MockStickyPackageAsync);
+ var client = CreateClient();
+
+ // 连接 TCP Server
+ var connect = await client.ConnectAsync("localhost", port);
+
+ var tcs = new TaskCompletionSource();
+ Memory receivedBuffer = Memory.Empty;
+
+ // 增加数据库处理适配器
+ client.SetDataHandler(new FixLengthDataPackageHandler(7)
+ {
+ ReceivedCallBack = buffer =>
+ {
+ receivedBuffer = buffer;
+ tcs.SetResult();
+ return Task.CompletedTask;
+ }
+ });
+
+ // 发送数据
+ var data = new Memory([1, 2, 3, 4, 5]);
+ 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]);
+
+ // 关闭连接
+ client.Close();
+ StopTcpServer(server);
+ }
+
+ 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 MockSplitPackageAsync(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[] { 0x3, 0x4 }, CancellationToken.None);
+ }
+ }
+
+ private static async Task MockStickyPackageAsync(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[] { 0x3, 0x4, 0x1, 0x2 }, CancellationToken.None);
+
+ // 模拟延时
+ await Task.Delay(50);
+
+ // 模拟粘包发送后续数据
+ await stream.WriteAsync(new byte[] { 0x3, 0x4, 0x5, 0x6, 0x7 }, CancellationToken.None);
+ }
+ }
+
+ private static Task MockAutoClosePackageAsync(TcpClient client)
+ {
+ client.Close();
+ return Task.CompletedTask;
+ }
+
+ private static void StopTcpServer(TcpListener server)
+ {
+ server?.Stop();
+ }
+
+ 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("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 async Task> SendAsync(Memory data)
+ {
+ Socket?.Close();
+ await Task.Delay(10);
+ return data;
+ }
+ }
+
+ class MockReceiveErrorHandler : DataPackageHandlerBase
+ {
+ public override Task> SendAsync(Memory data)
+ {
+ return Task.FromResult(data);
+ }
+
+ public override async Task ReceiveAsync(Memory data)
+ {
+ await base.ReceiveAsync(data);
+
+ // 模拟接收数据时报错
+ throw new InvalidOperationException("Test Error");
+ }
+ }
+}