From 489d284372364f0bb3a8ee4314f722514b4ada6c Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 18 Jun 2025 14:37:58 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20TouchSocket?= =?UTF-8?q?=20=E6=89=A9=E5=B1=95=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BootstrapBlazor.Extensions.sln | 7 + .../BootstrapBlazor.TouchSocket.csproj | 28 +++ .../Extensions/ServiceCollectionExtensions.cs | 25 +++ .../Services/DefaultTcpSocketClient.cs | 170 ++++++++++++++++++ .../Services/DefaultTcpSocketFactory.cs | 48 +++++ 5 files changed, 278 insertions(+) create mode 100644 src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj create mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs create mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs diff --git a/BootstrapBlazor.Extensions.sln b/BootstrapBlazor.Extensions.sln index d5ae678a..f4cc910e 100644 --- a/BootstrapBlazor.Extensions.sln +++ b/BootstrapBlazor.Extensions.sln @@ -192,6 +192,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.PdfViewer", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.Vditor", "src\components\BootstrapBlazor.Vditor\BootstrapBlazor.Vditor.csproj", "{D417E1B9-D146-4983-81D0-79F3193B322B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.TouchSocket", "src\extensions\BootstrapBlazor.TouchSocket\BootstrapBlazor.TouchSocket.csproj", "{FD23CEA1-78EB-85D7-8EDF-047657355B52}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -522,6 +524,10 @@ Global {D417E1B9-D146-4983-81D0-79F3193B322B}.Debug|Any CPU.Build.0 = Debug|Any CPU {D417E1B9-D146-4983-81D0-79F3193B322B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D417E1B9-D146-4983-81D0-79F3193B322B}.Release|Any CPU.Build.0 = Release|Any CPU + {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -612,6 +618,7 @@ Global {08458CA3-BF81-48E8-870D-9389DC037808} = {FF1089BE-C704-4374-B629-C57C08E1798F} {4757B038-70E4-40B0-9B73-700EE5632B07} = {FF1089BE-C704-4374-B629-C57C08E1798F} {D417E1B9-D146-4983-81D0-79F3193B322B} = {FF1089BE-C704-4374-B629-C57C08E1798F} + {FD23CEA1-78EB-85D7-8EDF-047657355B52} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF} diff --git a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj new file mode 100644 index 00000000..b95a8b44 --- /dev/null +++ b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj @@ -0,0 +1,28 @@ + + + + 9.0.0 + + + + Bootstrap Blazor WebAssembly wasm UI Components Topology FlowChart + Bootstrap UI components extensions of FlowChart + + + + + + + + + + + + + + + + + + + diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs b/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..54c948ed --- /dev/null +++ b/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using BootstrapBlazor.Components; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// BootstrapBlazor 服务扩展类 +/// +public static class BootstrapBlazorTouchSocketServiceExtensions +{ + /// + /// 添加 AzureOpenAIService 服务 + /// + /// + public static IServiceCollection AddBootstrapBlazorTouchSocketService(this IServiceCollection services) + { + services.AddSingleton(); + + // TBD: 这里注入 TouchSocket 相关服务 + return services; + } +} diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs new file mode 100644 index 00000000..eb668bfa --- /dev/null +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs @@ -0,0 +1,170 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Net; +using System.Net.Sockets; + +namespace BootstrapBlazor.Components; + +class DefaultTcpSocketClient : ITcpSocketClient +{ + private TcpClient? _client; + + public bool IsConnected => _client?.Connected ?? false; + + public IPEndPoint LocalEndPoint { get; } + + public ILogger? Logger { get; set; } + + public IDataPackageAdapter? DataPackageAdapter { get; set; } + + 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 : Dns.GetHostAddresses(host).FirstOrDefault() ?? IPAddress.Loopback; + + 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(); + + // 创建新的 TouchSocketClient 实例 + _client ??= new TcpClient(LocalEndPoint); + await _client.ConnectAsync(endPoint, token); + ret = true; + } + catch (OperationCanceledException ex) + { + LogWarning(ex, $"TCP Socket connect operation was canceled to {endPoint}"); + } + catch (Exception ex) + { + LogError(ex, $"TCP Socket connection failed to {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."); + } + + var ret = false; + try + { + var stream = _client.GetStream(); + await stream.WriteAsync(data, token); + ret = true; + } + catch (OperationCanceledException ex) + { + LogWarning(ex, $"TCP Socket send operation was canceled to {_client.Client.RemoteEndPoint}"); + } + catch (Exception ex) + { + LogError(ex, $"TCP Socket send failed to {_client.Client.RemoteEndPoint}"); + } + return ret; + } + + public async Task> ReceiveAsync(int bufferSize = 1024 * 10, CancellationToken token = default) + { + if (_client is not { Connected: true }) + { + throw new InvalidOperationException("TCP Socket is not connected."); + } + + var block = ArrayPool.Shared.Rent(bufferSize); + var buffer = new Memory(block); + try + { + var stream = _client.GetStream(); + var len = await stream.ReadAsync(buffer, token); + if (len == 0) + { + LogInformation($"TCP Socket received {len} data from {_client.Client.RemoteEndPoint}"); + } + else + { + buffer = buffer[..len]; + + if (DataPackageAdapter != null) + { + buffer = await DataPackageAdapter.ReceiveAsync(buffer); + } + } + } + catch (OperationCanceledException ex) + { + LogWarning(ex, $"TCP Socket receive operation was canceled to {_client.Client.RemoteEndPoint}"); + } + catch (Exception ex) + { + LogError(ex, $"TCP Socket receive failed to {_client.Client.RemoteEndPoint}"); + } + finally + { + ArrayPool.Shared.Return(block); + } + return buffer; + } + + public void Close() + { + Dispose(true); + } + + private void LogInformation(string message) + { + Logger?.LogInformation("{message}", message); + } + + private void LogWarning(Exception ex, string message) + { + Logger?.LogWarning(ex, "{message}", message); + } + + private void LogError(Exception ex, string message) + { + Logger?.LogError(ex, "{message}", message); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + if (_client != null) + { + _client.Close(); + _client = null; + } + } + } + + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs new file mode 100644 index 00000000..650f79cc --- /dev/null +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs @@ -0,0 +1,48 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace BootstrapBlazor.Components; + +class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory +{ + private readonly ConcurrentDictionary _pool = new(); + + public ITcpSocketClient GetOrCreate(string host, int port = 0, SocketMode mode = SocketMode.Client) + { + return _pool.GetOrAdd($"{host}:{port}", key => + { + var client = new DefaultTcpSocketClient(host, port) + { + Logger = provider.GetService>() + }; + 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); + } +} From e97d25902440c6650ef3cf672730a77258eb2436 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 16:05:22 +0800 Subject: [PATCH 02/17] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20DefaultT?= =?UTF-8?q?cpSocketFactory=20=E5=AE=9E=E7=8E=B0=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DefaultTcpSocketFactory.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs index 650f79cc..9f993a27 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs @@ -12,7 +12,7 @@ class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory { private readonly ConcurrentDictionary _pool = new(); - public ITcpSocketClient GetOrCreate(string host, int port = 0, SocketMode mode = SocketMode.Client) + public ITcpSocketClient GetOrCreate(string host, int port = 0) { return _pool.GetOrAdd($"{host}:{port}", key => { @@ -24,6 +24,16 @@ public ITcpSocketClient GetOrCreate(string host, int port = 0, SocketMode mode = }); } + 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) From 8b42331d035a77b58e7de58bd618d4353ea0c54c Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 19 Jun 2025 16:08:53 +0800 Subject: [PATCH 03/17] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=20DefaultT?= =?UTF-8?q?cpSocketClient=20=E5=AE=9E=E7=8E=B0=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DefaultTcpSocketClient.cs | 129 ++++++++++-------- 1 file changed, 74 insertions(+), 55 deletions(-) diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs index eb668bfa..83714aea 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs @@ -12,14 +12,18 @@ namespace BootstrapBlazor.Components; 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; } + public IPEndPoint LocalEndPoint { get; set; } + [NotNull] public ILogger? Logger { get; set; } - public IDataPackageAdapter? DataPackageAdapter { get; set; } + public int ReceiveBufferSize { get; set; } = 1024 * 10; public DefaultTcpSocketClient(string host, int port = 0) { @@ -28,7 +32,15 @@ public DefaultTcpSocketClient(string host, int port = 0) private static IPAddress GetIPAddress(string host) => host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ? IPAddress.Loopback - : IPAddress.TryParse(host, out var ip) ? ip : Dns.GetHostAddresses(host).FirstOrDefault() ?? 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) { @@ -44,18 +56,24 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke // 释放资源 Close(); - // 创建新的 TouchSocketClient 实例 + // 创建新的 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) { - LogWarning(ex, $"TCP Socket connect operation was canceled to {endPoint}"); + Logger.LogWarning(ex, "TCP Socket connect operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint); } catch (Exception ex) { - LogError(ex, $"TCP Socket connection failed to {endPoint}"); + Logger.LogError(ex, "TCP Socket connection failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint); } return ret; } @@ -64,67 +82,72 @@ public async Task SendAsync(Memory data, CancellationToken token = d { if (_client is not { Connected: true }) { - throw new InvalidOperationException("TCP Socket is not connected."); + 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) { - LogWarning(ex, $"TCP Socket send operation was canceled to {_client.Client.RemoteEndPoint}"); + Logger.LogWarning(ex, "TCP Socket send operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); } catch (Exception ex) { - LogError(ex, $"TCP Socket send failed to {_client.Client.RemoteEndPoint}"); + Logger.LogError(ex, "TCP Socket send failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); } return ret; } - public async Task> ReceiveAsync(int bufferSize = 1024 * 10, CancellationToken token = default) + private async Task ReceiveAsync() { - if (_client is not { Connected: true }) - { - throw new InvalidOperationException("TCP Socket is not connected."); - } - - var block = ArrayPool.Shared.Rent(bufferSize); - var buffer = new Memory(block); - try + _receiveCancellationTokenSource ??= new(); + while (_receiveCancellationTokenSource is { IsCancellationRequested: false }) { - var stream = _client.GetStream(); - var len = await stream.ReadAsync(buffer, token); - if (len == 0) + if (_client is not { Connected: true }) { - LogInformation($"TCP Socket received {len} data from {_client.Client.RemoteEndPoint}"); + throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); } - else - { - buffer = buffer[..len]; - if (DataPackageAdapter != null) + 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) { - buffer = await DataPackageAdapter.ReceiveAsync(buffer); + // 远端主机关闭链路 + 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); + } } - catch (OperationCanceledException ex) - { - LogWarning(ex, $"TCP Socket receive operation was canceled to {_client.Client.RemoteEndPoint}"); - } - catch (Exception ex) - { - LogError(ex, $"TCP Socket receive failed to {_client.Client.RemoteEndPoint}"); - } - finally - { - ArrayPool.Shared.Return(block); - } - return buffer; } public void Close() @@ -132,25 +155,21 @@ public void Close() Dispose(true); } - private void LogInformation(string message) - { - Logger?.LogInformation("{message}", message); - } - - private void LogWarning(Exception ex, string message) - { - Logger?.LogWarning(ex, "{message}", message); - } - - private void LogError(Exception ex, string message) - { - Logger?.LogError(ex, "{message}", message); - } - 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(); From 29298c98b37e7b81edf680b58790688fc279a82a Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 20 Jun 2025 11:07:45 +0800 Subject: [PATCH 04/17] =?UTF-8?q?refactor:=20=E6=A0=B9=E6=8D=AE=E6=9C=80?= =?UTF-8?q?=E6=96=B0=E8=AE=BE=E8=AE=A1=E6=9B=B4=E6=94=B9=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DefaultTcpSocketClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs index 83714aea..e9377a17 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs @@ -42,13 +42,13 @@ public void SetDataHandler(IDataPackageHandler handler) _dataPackageHandler = handler; } - public Task ConnectAsync(string host, int port, CancellationToken token = default) + public ValueTask 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) + public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) { var ret = false; try @@ -78,7 +78,7 @@ public async Task ConnectAsync(IPEndPoint endPoint, CancellationToken toke return ret; } - public async Task SendAsync(Memory data, CancellationToken token = default) + public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) { if (_client is not { Connected: true }) { From 4681d4d07037afee7cdaaa99aa0ded01b9bdcfc7 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 20 Jun 2025 11:15:45 +0800 Subject: [PATCH 05/17] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E4=BE=9D=E8=B5=96=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BootstrapBlazor.TouchSocket.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj index b95a8b44..e58244e8 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj +++ b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj @@ -14,7 +14,7 @@ - + From 9f47065f0fd6a0e9aeee6c3d211ecdbd3d01e4d2 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 20 Jun 2025 12:09:40 +0800 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=E6=A0=B9=E6=8D=AE=E6=96=B0?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=AE=9A=E4=B9=89=E5=A2=9E=E5=8A=A0=20Receiv?= =?UTF-8?q?edCallBack=20=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DefaultTcpSocketClient.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs index e9377a17..a05ba997 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs @@ -37,6 +37,8 @@ private static IPAddress GetIPAddress(string host) => host.Equals("localhost", S [ExcludeFromCodeCoverage] private static IPAddress IPAddressByHostName => Dns.GetHostAddresses(Dns.GetHostName(), AddressFamily.InterNetwork).FirstOrDefault() ?? IPAddress.Loopback; + public Func, ValueTask>? ReceivedCallBack { get; set; } + public void SetDataHandler(IDataPackageHandler handler) { _dataPackageHandler = handler; From b310fed502017c148ccc6f5200f3e1a622fc8e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=A5=E6=B1=9D=E6=A3=8B=E8=8C=97?= <505554090@qq.com> Date: Fri, 20 Jun 2025 21:25:16 +0800 Subject: [PATCH 07/17] =?UTF-8?q?feat(tcp):=20=E6=9B=BF=E6=8D=A2=20Default?= =?UTF-8?q?TcpSocketClient=20=E4=B8=BA=20TouchSocketTcpClient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 TouchSocket 包引用,删除 DefaultTcpSocketClient 类,使用 TouchSocketTcpClient 类实现 ITcpSocketClient 接口,提供连接、发送和接收数据的功能。修改 DefaultTcpSocketFactory 类构造函数,移除对 ILogger 的依赖,改为使用 TouchSocketTcpClient。新增 TouchSocketTcpClient 类实现 TCP 连接逻辑。 --- .../BootstrapBlazor.TouchSocket.csproj | 6 +- .../Services/DefaultTcpSocketClient.cs | 191 ------------------ .../Services/DefaultTcpSocketFactory.cs | 7 +- .../Services/TouchSocketTcpClient.cs | 144 +++++++++++++ 4 files changed, 151 insertions(+), 197 deletions(-) delete mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs create mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Services/TouchSocketTcpClient.cs diff --git a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj index e58244e8..4368fd49 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj +++ b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj @@ -14,13 +14,17 @@ - + + + + + diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs deleted file mode 100644 index a05ba997..00000000 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Website: https://www.blazor.zone or https://argozhang.github.io/ - -using Microsoft.Extensions.Logging; -using System.Buffers; -using System.Net; -using System.Net.Sockets; - -namespace BootstrapBlazor.Components; - -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 Func, ValueTask>? ReceivedCallBack { get; set; } - - public void SetDataHandler(IDataPackageHandler handler) - { - _dataPackageHandler = handler; - } - - public ValueTask ConnectAsync(string host, int port, CancellationToken token = default) - { - var endPoint = new IPEndPoint(GetIPAddress(host), port); - return ConnectAsync(endPoint, token); - } - - public async ValueTask 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 ValueTask SendAsync(ReadOnlyMemory 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/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs index 9f993a27..fcf61165 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs @@ -8,7 +8,7 @@ namespace BootstrapBlazor.Components; -class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory +class DefaultTcpSocketFactory : ITcpSocketFactory { private readonly ConcurrentDictionary _pool = new(); @@ -16,10 +16,7 @@ public ITcpSocketClient GetOrCreate(string host, int port = 0) { return _pool.GetOrAdd($"{host}:{port}", key => { - var client = new DefaultTcpSocketClient(host, port) - { - Logger = provider.GetService>() - }; + var client = new TouchSocketTcpClient(); return client; }); } diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/TouchSocketTcpClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/TouchSocketTcpClient.cs new file mode 100644 index 00000000..fc9964d7 --- /dev/null +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/TouchSocketTcpClient.cs @@ -0,0 +1,144 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using TouchSocket.Core; +using TouchSocket.Sockets; + +namespace BootstrapBlazor.Components; + +internal sealed class TouchSocketTcpClient : TcpClientBase, ITcpSocketClient +{ + private IDataPackageHandler? dataPackageHandler; + public bool IsConnected => base.Online; + public IPEndPoint LocalEndPoint => base.MainSocket.LocalEndPoint as IPEndPoint ?? throw new ArgumentNullException(nameof(LocalEndPoint)); + public int ReceiveBufferSize { get; set; } + public Func, ValueTask>? ReceivedCallBack { get; set; } + + #region ConnectAsync + + public async ValueTask ConnectAsync(string host, int port, CancellationToken token = default) + { + try + { + if (this.IsConnected) + { + await this.CloseAsync("Already connected", token); + } + + await this.SetupAsync(new TouchSocketConfig() + .SetRemoteIPHost($"{host}:{port}")); + + await base.TcpConnectAsync(5000, token); + + return true; + } + catch + { + return false; + } + } + + public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + try + { + if (this.IsConnected) + { + await this.CloseAsync("Already connected", token); + } + + await this.SetupAsync(new TouchSocketConfig() + .SetRemoteIPHost(endPoint.ToString())); + + await base.TcpConnectAsync(5000, token); + + return true; + } + catch + { + return false; + } + } + + #endregion ConnectAsync + + public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + try + { + ReadOnlyMemory memory; + var dataPackageHandler = this.dataPackageHandler; + if (dataPackageHandler == null) + { + memory = data; + } + else + { + memory = await dataPackageHandler.SendAsync(data); + } + + await base.ProtectedDefaultSendAsync(memory).WaitAsync(token); + return true; + } + catch + { + return false; + } + } + + public void SetDataHandler(IDataPackageHandler handler) + { + this.dataPackageHandler = handler ?? throw new ArgumentNullException(nameof(handler)); + handler.ReceivedCallBack = this.OnReceivedCallBack; + } + + protected override async ValueTask OnTcpReceiving(ByteBlock byteBlock) + { + var dataPackageHandler = this.dataPackageHandler; + if (dataPackageHandler != null) + { + await dataPackageHandler.ReceiveAsync(byteBlock.Memory); + } + + var func = this.ReceivedCallBack; + if (func != null) + { + await func(byteBlock.Memory); + } + return true; + } + + private async ValueTask OnReceivedCallBack(ReadOnlyMemory memory) + { + var func = this.ReceivedCallBack; + if (func != null) + { + await func(memory); + } + } + + #region Close + + async ValueTask ITcpSocketClient.CloseAsync(string msg, CancellationToken token) + { + await this.CloseAsync(msg, token); + return true; + } + + public override async Task CloseAsync(string msg, CancellationToken token = default) + { + await base.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both); + return await base.CloseAsync(msg, token); + } + + #endregion Close +} From 3016e14f6d40239b6da6847973079eb3130c3ad7 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 23 Jun 2025 14:43:14 +0800 Subject: [PATCH 08/17] =?UTF-8?q?wip:=20=E9=87=8D=E6=9E=84=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E6=96=B9=E6=B3=95=EF=BC=88=E6=9C=AA=E5=AE=8C=E6=88=90?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DefaultTcpSocketClient.cs | 181 ++++++++++++++++++ .../Services/DefaultTcpSocketFactory.cs | 19 +- .../Services/TouchSocketTcpClient.cs | 144 -------------- 3 files changed, 194 insertions(+), 150 deletions(-) create mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs delete mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Services/TouchSocketTcpClient.cs diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs new file mode 100644 index 00000000..448aabe4 --- /dev/null +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs @@ -0,0 +1,181 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Net; +using TouchSocket.Core; +using TouchSocket.Sockets; + +namespace BootstrapBlazor.Components; + +sealed class DefaultTcpSocketClient(IPEndPoint endPoint) : ITcpSocketClient +{ + private TcpClient? _client; + private IDataPackageHandler? _dataPackageHandler; + private CancellationTokenSource? _receiveCancellationTokenSource; + private IPEndPoint? _remoteEndPoint; + + public bool IsConnected => _client?.Online ?? false; + + public IPEndPoint LocalEndPoint { get; set; } = endPoint; + + [NotNull] + public ILogger? Logger { get; set; } + + public int ReceiveBufferSize { get; set; } = 1024 * 10; + + public Func, ValueTask>? ReceivedCallBack { get; set; } + + public void SetDataHandler(IDataPackageHandler handler) + { + _dataPackageHandler = handler; + } + + public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + var ret = false; + try + { + // 释放资源 + Close(); + + // 创建新的 TouchSocket TcpClient 实例 + _client ??= new TcpClient(); + await _client.SetupAsync(new TouchSocketConfig().SetBindIPHost(new IPHost(LocalEndPoint.Address, LocalEndPoint.Port))); + + await _client.ConnectAsync(new IPHost(endPoint.Address, endPoint.Port), ); + + // 开始接收数据 + //_ = 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 ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + if (_client is not { Online: 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 ValueTask ReceiveAsync() + { + _receiveCancellationTokenSource ??= new(); + while (_receiveCancellationTokenSource is { IsCancellationRequested: false }) + { + if (_client is not { Online: 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 (ReceivedCallBack != null) + // { + // await ReceivedCallBack(buffer); + // } + + // 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.CloseAsync(); + _client = null; + } + } + } + + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs index fcf61165..0aec4429 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs @@ -5,26 +5,33 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; +using System.Net; +using System.Runtime.Versioning; namespace BootstrapBlazor.Components; -class DefaultTcpSocketFactory : ITcpSocketFactory +[UnsupportedOSPlatform("browser")] +sealed class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory { private readonly ConcurrentDictionary _pool = new(); - public ITcpSocketClient GetOrCreate(string host, int port = 0) + public ITcpSocketClient GetOrCreate(string name, Func valueFactory) { - return _pool.GetOrAdd($"{host}:{port}", key => + return _pool.GetOrAdd(name, key => { - var client = new TouchSocketTcpClient(); + var endPoint = valueFactory(key); + var client = new DefaultTcpSocketClient(endPoint) + { + Logger = provider.GetService>() + }; return client; }); } - public ITcpSocketClient? Remove(string host, int port) + public ITcpSocketClient? Remove(string name) { ITcpSocketClient? client = null; - if (_pool.TryRemove($"{host}:{port}", out var c)) + if (_pool.TryRemove(name, out var c)) { client = c; } diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/TouchSocketTcpClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/TouchSocketTcpClient.cs deleted file mode 100644 index fc9964d7..00000000 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/TouchSocketTcpClient.cs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Website: https://www.blazor.zone or https://argozhang.github.io/ - -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using TouchSocket.Core; -using TouchSocket.Sockets; - -namespace BootstrapBlazor.Components; - -internal sealed class TouchSocketTcpClient : TcpClientBase, ITcpSocketClient -{ - private IDataPackageHandler? dataPackageHandler; - public bool IsConnected => base.Online; - public IPEndPoint LocalEndPoint => base.MainSocket.LocalEndPoint as IPEndPoint ?? throw new ArgumentNullException(nameof(LocalEndPoint)); - public int ReceiveBufferSize { get; set; } - public Func, ValueTask>? ReceivedCallBack { get; set; } - - #region ConnectAsync - - public async ValueTask ConnectAsync(string host, int port, CancellationToken token = default) - { - try - { - if (this.IsConnected) - { - await this.CloseAsync("Already connected", token); - } - - await this.SetupAsync(new TouchSocketConfig() - .SetRemoteIPHost($"{host}:{port}")); - - await base.TcpConnectAsync(5000, token); - - return true; - } - catch - { - return false; - } - } - - public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) - { - try - { - if (this.IsConnected) - { - await this.CloseAsync("Already connected", token); - } - - await this.SetupAsync(new TouchSocketConfig() - .SetRemoteIPHost(endPoint.ToString())); - - await base.TcpConnectAsync(5000, token); - - return true; - } - catch - { - return false; - } - } - - #endregion ConnectAsync - - public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) - { - try - { - ReadOnlyMemory memory; - var dataPackageHandler = this.dataPackageHandler; - if (dataPackageHandler == null) - { - memory = data; - } - else - { - memory = await dataPackageHandler.SendAsync(data); - } - - await base.ProtectedDefaultSendAsync(memory).WaitAsync(token); - return true; - } - catch - { - return false; - } - } - - public void SetDataHandler(IDataPackageHandler handler) - { - this.dataPackageHandler = handler ?? throw new ArgumentNullException(nameof(handler)); - handler.ReceivedCallBack = this.OnReceivedCallBack; - } - - protected override async ValueTask OnTcpReceiving(ByteBlock byteBlock) - { - var dataPackageHandler = this.dataPackageHandler; - if (dataPackageHandler != null) - { - await dataPackageHandler.ReceiveAsync(byteBlock.Memory); - } - - var func = this.ReceivedCallBack; - if (func != null) - { - await func(byteBlock.Memory); - } - return true; - } - - private async ValueTask OnReceivedCallBack(ReadOnlyMemory memory) - { - var func = this.ReceivedCallBack; - if (func != null) - { - await func(memory); - } - } - - #region Close - - async ValueTask ITcpSocketClient.CloseAsync(string msg, CancellationToken token) - { - await this.CloseAsync(msg, token); - return true; - } - - public override async Task CloseAsync(string msg, CancellationToken token = default) - { - await base.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both); - return await base.CloseAsync(msg, token); - } - - #endregion Close -} From bf3eb630a2d62b196663b132b583166b792f8e5d Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 24 Jun 2025 10:24:19 +0800 Subject: [PATCH 09/17] =?UTF-8?q?wip:=20=E4=B8=B4=E6=97=B6=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BootstrapBlazor.Extensions.sln | 7 ++ ...ootstrapBlazor.OfficeDocumentViewer.csproj | 21 ++++ .../OfficeDocumentViewer.razor | 5 + .../OfficeDocumentViewer.razor.cs | 103 ++++++++++++++++++ .../OfficeDocumentViewer.razor.js | 37 +++++++ .../_Imports.razor | 2 + .../wwwroot/office-viewer.css | 9 ++ 7 files changed, 184 insertions(+) create mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/BootstrapBlazor.OfficeDocumentViewer.csproj create mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor create mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.cs create mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js create mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/_Imports.razor create mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/wwwroot/office-viewer.css diff --git a/BootstrapBlazor.Extensions.sln b/BootstrapBlazor.Extensions.sln index f4cc910e..f69f1934 100644 --- a/BootstrapBlazor.Extensions.sln +++ b/BootstrapBlazor.Extensions.sln @@ -194,6 +194,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.Vditor", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.TouchSocket", "src\extensions\BootstrapBlazor.TouchSocket\BootstrapBlazor.TouchSocket.csproj", "{FD23CEA1-78EB-85D7-8EDF-047657355B52}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.OfficeDocumentViewer", "src\components\BootstrapBlazor.OfficeDocumentViewer\BootstrapBlazor.OfficeDocumentViewer.csproj", "{B2B4B85C-06AE-411C-8E29-C9A4AF473548}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -528,6 +530,10 @@ Global {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Release|Any CPU.Build.0 = Release|Any CPU + {B2B4B85C-06AE-411C-8E29-C9A4AF473548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2B4B85C-06AE-411C-8E29-C9A4AF473548}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2B4B85C-06AE-411C-8E29-C9A4AF473548}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2B4B85C-06AE-411C-8E29-C9A4AF473548}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -619,6 +625,7 @@ Global {4757B038-70E4-40B0-9B73-700EE5632B07} = {FF1089BE-C704-4374-B629-C57C08E1798F} {D417E1B9-D146-4983-81D0-79F3193B322B} = {FF1089BE-C704-4374-B629-C57C08E1798F} {FD23CEA1-78EB-85D7-8EDF-047657355B52} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} + {B2B4B85C-06AE-411C-8E29-C9A4AF473548} = {FF1089BE-C704-4374-B629-C57C08E1798F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF} diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/BootstrapBlazor.OfficeDocumentViewer.csproj b/src/components/BootstrapBlazor.OfficeDocumentViewer/BootstrapBlazor.OfficeDocumentViewer.csproj new file mode 100644 index 00000000..0f96d940 --- /dev/null +++ b/src/components/BootstrapBlazor.OfficeDocumentViewer/BootstrapBlazor.OfficeDocumentViewer.csproj @@ -0,0 +1,21 @@ + + + + 9.0.0 + + + + Bootstrap Blazor WebAssembly wasm UI Components Office Viewer + Bootstrap UI components extensions of Microsoft Office Documentation Viewer + + + + + + + + + + + + diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor b/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor new file mode 100644 index 00000000..b12ca85d --- /dev/null +++ b/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor @@ -0,0 +1,5 @@ +@namespace BootstrapBlazor.Components +@inherits BootstrapModuleComponentBase +@attribute [JSModuleAutoLoader("./_content/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js", JSObjectReference = true, AutoInvokeDispose = false)] + +
diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.cs b/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.cs new file mode 100644 index 00000000..14cd3192 --- /dev/null +++ b/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.cs @@ -0,0 +1,103 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.AspNetCore.Components; + +namespace BootstrapBlazor.Components; + +public partial class OfficeDocumentViewer +{ + /// + /// Gets or sets the url for the PDF file to be displayed. + /// + [Parameter] + public string? Url { get; set; } + + /// + /// Gets or sets the viewer height. Default is null. + /// + [Parameter] + public string? Height { get; set; } + + /// + /// Gets or sets the document loaded event callback. + /// + [Parameter] + public Func? OnLoaded { get; set; } + + [Inject, NotNull] + private NavigationManager? NavigationManager { get; set; } + + private string? ClassString => CssBuilder.Default("bb-office-viewer-container") + .AddClassFromAttributes(AdditionalAttributes) + .Build(); + + private string? StyleString => CssBuilder.Default() + .AddClass($"--bb-office-viewer-height: {Height};", !string.IsNullOrEmpty(Height)) + .Build(); + + private string? _url; + + /// + /// + /// + /// + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender) + { + _url = Url; + return; + } + + var rerender = false; + if (_url != Url) + { + _url = Url; + rerender = true; + } + + if (rerender) + { + await InvokeVoidAsync("load", Id, GetAbsoluteUri(_url)); + } + } + + /// + /// + /// + /// + protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new + { + LoadedCallaback = nameof(TriggerOnLoaded), + Url = GetAbsoluteUri(Url) + }); + + private string GetAbsoluteUri(string? url) + { + url ??= string.Empty; + if (string.IsNullOrEmpty(url)) + { + return url; + } + var uri = NavigationManager.ToAbsoluteUri(url); + return uri.AbsoluteUri; + } + + /// + /// Trigger OnLoaded callback when the PDF document is loaded. + /// + /// + [JSInvokable] + public async Task TriggerOnLoaded() + { + if (OnLoaded != null) + { + await OnLoaded(); + } + } +} diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js b/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js new file mode 100644 index 00000000..b9f8b87f --- /dev/null +++ b/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js @@ -0,0 +1,37 @@ +import { addLink } from "../BootstrapBlazor/modules/utility.js" +import Data from "../BootstrapBlazor/modules/data.js" + +export async function init(id, invoke, options) { + await addLink("./_content/BootstrapBlazor.OfficeDocumentViewer/office-viewer.css"); + + const el = document.getElementById(id); + const officeViewer = { el, invoke, options }; + Data.set(id, officeViewer); + + await load(id, options.url); +} + +export async function load(id, url) { + const officeViewer = Data.get(id); + const { el, invoke, options } = officeViewer; + + el.innerHTML = ''; + + if (url) { + const { frame } = officeViewer; + const viewer = frame || createFrame(el); + if (options.loadedCallaback) { + viewer.onload = () => { + invoke.invokeMethodAsync(options.loadedCallaback); + }; + } + viewer.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`; + } +} + +const createFrame = el => { + const frame = document.createElement('iframe'); + frame.classList.add('bb-office-viewer'); + el.appendChild(frame); + return frame; +} diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/_Imports.razor b/src/components/BootstrapBlazor.OfficeDocumentViewer/_Imports.razor new file mode 100644 index 00000000..d46585f8 --- /dev/null +++ b/src/components/BootstrapBlazor.OfficeDocumentViewer/_Imports.razor @@ -0,0 +1,2 @@ +@using BootstrapBlazor.Components; +@using Microsoft.AspNetCore.Components.Web diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/wwwroot/office-viewer.css b/src/components/BootstrapBlazor.OfficeDocumentViewer/wwwroot/office-viewer.css new file mode 100644 index 00000000..0cdbffd0 --- /dev/null +++ b/src/components/BootstrapBlazor.OfficeDocumentViewer/wwwroot/office-viewer.css @@ -0,0 +1,9 @@ +.bb-office-viewer-container { + width: 100%; + height: var(--bb-office-viewer-height, 500px); +} + +.bb-office-viewer { + width: 100%; + height: 100%; +} From 1c5e7c3c869865ad971aae0842f47bd8cca0b01d Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 27 Jun 2025 15:04:02 +0800 Subject: [PATCH 10/17] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=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 --- BootstrapBlazor.Extensions.sln | 7 + .../TcpSocketFactoryTest.cs | 658 ++++++++++++++++++ .../UnitTestTouchSocket.csproj | 13 + 3 files changed, 678 insertions(+) create mode 100644 test/UnitTestTouchSocket/TcpSocketFactoryTest.cs create mode 100644 test/UnitTestTouchSocket/UnitTestTouchSocket.csproj diff --git a/BootstrapBlazor.Extensions.sln b/BootstrapBlazor.Extensions.sln index f062a7e4..c91fe4d9 100644 --- a/BootstrapBlazor.Extensions.sln +++ b/BootstrapBlazor.Extensions.sln @@ -196,6 +196,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.TouchSocket EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.OfficeViewer", "src\components\BootstrapBlazor.OfficeViewer\BootstrapBlazor.OfficeViewer.csproj", "{2436940C-5920-D801-8A81-721F4C20A355}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestTouchSocket", "test\UnitTestTouchSocket\UnitTestTouchSocket.csproj", "{23D21A45-64A3-704C-27C0-73399048ED03}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -534,6 +536,10 @@ Global {2436940C-5920-D801-8A81-721F4C20A355}.Debug|Any CPU.Build.0 = Debug|Any CPU {2436940C-5920-D801-8A81-721F4C20A355}.Release|Any CPU.ActiveCfg = Release|Any CPU {2436940C-5920-D801-8A81-721F4C20A355}.Release|Any CPU.Build.0 = Release|Any CPU + {23D21A45-64A3-704C-27C0-73399048ED03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23D21A45-64A3-704C-27C0-73399048ED03}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23D21A45-64A3-704C-27C0-73399048ED03}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23D21A45-64A3-704C-27C0-73399048ED03}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -626,6 +632,7 @@ Global {D417E1B9-D146-4983-81D0-79F3193B322B} = {FF1089BE-C704-4374-B629-C57C08E1798F} {FD23CEA1-78EB-85D7-8EDF-047657355B52} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} {2436940C-5920-D801-8A81-721F4C20A355} = {FF1089BE-C704-4374-B629-C57C08E1798F} + {23D21A45-64A3-704C-27C0-73399048ED03} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF} diff --git a/test/UnitTestTouchSocket/TcpSocketFactoryTest.cs b/test/UnitTestTouchSocket/TcpSocketFactoryTest.cs new file mode 100644 index 00000000..b85ef674 --- /dev/null +++ b/test/UnitTestTouchSocket/TcpSocketFactoryTest.cs @@ -0,0 +1,658 @@ +// 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.Text; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace UnitTestTouchSocket; + +public class TcpSocketFactoryTest +{ + [Fact] + public async Task GetOrCreate_Ok() + { + // 测试 GetOrCreate 方法创建的 Client 销毁后继续 GetOrCreate 得到的对象是否可用 + var sc = new ServiceCollection(); + sc.AddLogging(builder => + { + builder.AddProvider(new MockLoggerProvider()); + }); + sc.AddBootstrapBlazorTouchSocketService(); + var provider = sc.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + var client1 = factory.GetOrCreate("demo", key => Utility.ConvertToIpEndPoint("localhost", 0)); + await client1.CloseAsync(); + + var client2 = factory.GetOrCreate("demo", key => Utility.ConvertToIpEndPoint("localhost", 0)); + Assert.Equal(client1, client2); + + var ip = Dns.GetHostAddresses(Dns.GetHostName(), AddressFamily.InterNetwork).FirstOrDefault() ?? IPAddress.Loopback; + var client3 = factory.GetOrCreate("demo1", key => Utility.ConvertToIpEndPoint(ip.ToString(), 0)); + + // 测试不合格 IP 地址 + var client4 = factory.GetOrCreate("demo2", key => Utility.ConvertToIpEndPoint("256.0.0.1", 0)); + + var client5 = factory.Remove("demo2"); + Assert.Equal(client4, client5); + Assert.NotNull(client5); + + await client5.DisposeAsync(); + await factory.DisposeAsync(); + } + + [Fact] + public async Task ConnectAsync_Timeout() + { + var client = CreateClient(); + client.ConnectTimeout = 1000; + + var connect = await client.ConnectAsync("localhost", 9999); + Assert.False(connect); + } + + [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 Send_Timeout() + { + var port = 8887; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(); + client.SendTimeout = 100; + client.SetDataHandler(new MockSendTimeoutHandler()); + + await client.ConnectAsync("localhost", port); + + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + var result = await client.SendAsync(data); + Assert.False(result); + } + + [Fact] + public async Task SendAsync_Error() + { + var client = CreateClient(); + + // 测试未建立连接前调用 SendAsync 方法报异常逻辑 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + var ex = await Assert.ThrowsAsync(async () => await client.SendAsync(data)); + Assert.NotNull(ex); + } + + [Fact] + public async Task SendAsync_Cancel() + { + var port = 8881; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(nameof(SendAsync_Cancel)); + Assert.False(client.IsConnected); + + // 连接 TCP Server + await client.ConnectAsync("localhost", port); + Assert.True(client.IsConnected); + + // 测试 SendAsync 方法发送取消逻辑 + var cst = new CancellationTokenSource(); + cst.Cancel(); + + var result = await client.SendAsync("test", null, 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("test", Encoding.UTF8); + tcs.SetResult(); + }); + + await tcs.Task; + Assert.False(sendResult); + + // 关闭连接 + StopTcpServer(server); + } + + [Fact] + public async Task ReceiveAsync_Timeout() + { + var port = 8888; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(); + client.ReceiveTimeout = 100; + client.SetDataHandler(new MockReceiveTimeoutHandler()); + + await client.ConnectAsync("localhost", port); + + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + await Task.Delay(220); // 等待接收超时 + } + + [Fact] + public async Task ReceiveAsync_Cancel() + { + var port = 8889; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(nameof(ReceiveAsync_Cancel)); + client.SetDataHandler(new MockReceiveTimeoutHandler()); + + await client.ConnectAsync("localhost", port); + + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + // 通过反射取消令牌 + var fieldInfo = client.GetType().GetField("_receiveCancellationTokenSource", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(fieldInfo); + var tokenSource = fieldInfo.GetValue(client) as CancellationTokenSource; + Assert.NotNull(tokenSource); + tokenSource.Cancel(); + await Task.Delay(50); + } + + [Fact] + public async Task ReceiveAsync_InvalidOperationException() + { + var port = 8890; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(); + + //未连接 + var ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); + Assert.NotNull(ex); + + // 反射给 _client 赋值但是未连接 + var fieldInfo = client.GetType().GetField("_client", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(fieldInfo); + fieldInfo.SetValue(client, new TouchSocket.Sockets.TcpClient()); + ex = null; + ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); + Assert.NotNull(ex); + + await client.ConnectAsync("localhost", port); + ex = null; + ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); + + await client.CloseAsync(); + client.IsAutoReceive = false; + var connected = await client.ConnectAsync("localhost", port); + Assert.True(connected); + + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + var payload = await client.ReceiveAsync(); + Assert.Equal(payload.ToArray(), [1, 2, 3, 4, 5]); + } + + [Fact] + public async Task ReceiveAsync_Ok() + { + var port = 8891; + var server = StartTcpServer(port, MockSplitPackageAsync); + + var client = CreateClient(); + client.IsAutoReceive = false; + await client.ConnectAsync("localhost", port); + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + var payload = await client.ReceiveAsync(); + Assert.Equal(payload.ToArray(), [1, 2, 3, 4, 5]); + } + + [Fact] + public async Task ReceiveAsync_Error() + { + var client = CreateClient(); + + // 测试未建立连接前调用 ReceiveAsync 方法报异常逻辑 + var methodInfo = client.GetType().GetMethod("AutoReceiveAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(methodInfo); + + var task = (ValueTask)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 * 64, client.ReceiveBufferSize); + + client.ReceiveBufferSize = 1024 * 20; + Assert.Equal(1024 * 20, client.ReceiveBufferSize); + + client.SetDataHandler(new MockReceiveErrorHandler()); + + ReadOnlyMemory buffer = ReadOnlyMemory.Empty; + var tcs = new TaskCompletionSource(); + + // 增加接收回调方法 + client.ReceivedCallBack = b => + { + buffer = b; + tcs.SetResult(); + return ValueTask.CompletedTask; + }; + + await client.ConnectAsync("localhost", port); + + // 发送数据导致接收数据异常 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + await tcs.Task; + Assert.Equal(buffer.ToArray(), [1, 2, 3, 4, 5]); + + // 关闭连接 + StopTcpServer(server); + } + + [Fact] + public async Task CloseByRemote_Ok() + { + var client = CreateClient(nameof(CloseByRemote_Ok)); + + var port = 8883; + var server = StartTcpServer(port, MockAutoClosePackageAsync); + + client.SetDataHandler(new MockReceiveErrorHandler()); + + // 连接 TCP Server + await client.ConnectAsync("localhost", port); + + // 发送数据 + await client.SendAsync(new ReadOnlyMemory([1, 2, 3, 4, 5])); + + // 关闭连接 + StopTcpServer(server); + } + + [Fact] + public async Task FixLengthDataPackageHandler_Ok() + { + var port = 8884; + 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(); + ReadOnlyMemory receivedBuffer = ReadOnlyMemory.Empty; + + // 增加数据处理适配器 + client.SetDataHandler(new FixLengthDataPackageHandler(7) + { + ReceivedCallBack = buffer => + { + receivedBuffer = buffer; + tcs.SetResult(); + return ValueTask.CompletedTask; + } + }); + + // 测试 SendAsync 方法 + var data = new ReadOnlyMemory([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); + + // 关闭连接 + await client.CloseAsync(); + StopTcpServer(server); + } + + [Fact] + public async Task FixLengthDataPackageHandler_Sticky() + { + var port = 8885; + var server = StartTcpServer(port, MockStickyPackageAsync); + var client = CreateClient(); + + // 连接 TCP Server + var connect = await client.ConnectAsync("localhost", port); + + var tcs = new TaskCompletionSource(); + ReadOnlyMemory receivedBuffer = ReadOnlyMemory.Empty; + + // 增加数据库处理适配器 + client.SetDataHandler(new FixLengthDataPackageHandler(7) + { + ReceivedCallBack = buffer => + { + receivedBuffer = buffer; + tcs.SetResult(); + return ValueTask.CompletedTask; + } + }); + + // 发送数据 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + // 等待接收数据处理完成 + await tcs.Task; + + // 验证接收到的数据 + Assert.Equal(receivedBuffer.ToArray(), [1, 2, 3, 4, 5, 3, 4]); + receivedBuffer = ReadOnlyMemory.Empty; + tcs = new TaskCompletionSource(); + + // 等待第二次数据 + await tcs.Task; + + // 验证第二次收到的数据 + Assert.Equal(receivedBuffer.ToArray(), [2, 2, 3, 4, 5, 6, 7]); + tcs = new TaskCompletionSource(); + await tcs.Task; + + // 验证第三次收到的数据 + Assert.Equal(receivedBuffer.ToArray(), [3, 2, 3, 4, 5, 6, 7]); + + // 关闭连接 + await client.CloseAsync(); + StopTcpServer(server); + } + + [Fact] + public async Task DelimiterDataPackageHandler_Ok() + { + var port = 8886; + var server = StartTcpServer(port, MockDelimiterPackageAsync); + var client = CreateClient(); + + // 连接 TCP Server + var connect = await client.ConnectAsync("localhost", port); + + var tcs = new TaskCompletionSource(); + var receivedBuffer = ReadOnlyMemory.Empty; + + // 增加数据库处理适配器 + client.SetDataHandler(new DelimiterDataPackageHandler([0x13, 0x10]) + { + ReceivedCallBack = buffer => + { + receivedBuffer = buffer; + tcs.SetResult(); + return ValueTask.CompletedTask; + } + }); + + // 发送数据 + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); + await client.SendAsync(data); + + // 等待接收数据处理完成 + await tcs.Task; + + // 验证接收到的数据 + Assert.Equal(receivedBuffer.ToArray(), [1, 2, 3, 4, 5, 0x13, 0x10]); + + // 等待第二次数据 + receivedBuffer = ReadOnlyMemory.Empty; + tcs = new TaskCompletionSource(); + await tcs.Task; + + // 验证接收到的数据 + Assert.Equal(receivedBuffer.ToArray(), [5, 6, 0x13, 0x10]); + + // 关闭连接 + await client.CloseAsync(); + StopTcpServer(server); + + var handler = new DelimiterDataPackageHandler("\r\n"); + var ex = Assert.Throws(() => new DelimiterDataPackageHandler(string.Empty)); + Assert.NotNull(ex); + + ex = Assert.Throws(() => new DelimiterDataPackageHandler((byte[])null!)); + Assert.NotNull(ex); + } + + 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[1024]; + 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 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 ReadOnlyMemory(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 ReadOnlyMemory(buffer, 0, len); + await stream.WriteAsync(block, CancellationToken.None); + + // 模拟延时 + await Task.Delay(10); + + // 模拟拆包发送第二段数据 + await stream.WriteAsync(new byte[] { 0x3, 0x4, 0x2, 0x2 }, CancellationToken.None); + + // 模拟延时 + await Task.Delay(10); + + // 模拟粘包发送后续数据 + await stream.WriteAsync(new byte[] { 0x3, 0x4, 0x5, 0x6, 0x7, 0x3, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x1 }, CancellationToken.None); + } + } + + private static async Task MockAutoClosePackageAsync(TcpClient client) + { + // 增加延时防止 Send 方法抛异常 + await Task.Delay(10); + client.Close(); + } + + private static void StopTcpServer(TcpListener server) + { + server?.Stop(); + } + + private static ITcpSocketClient CreateClient(string name = "test") + { + var sc = new ServiceCollection(); + sc.AddLogging(builder => + { + builder.AddProvider(new MockLoggerProvider()); + }); + sc.AddBootstrapBlazorTouchSocketService(); + var provider = sc.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + var client = factory.GetOrCreate(name, key => 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 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/UnitTestTouchSocket/UnitTestTouchSocket.csproj b/test/UnitTestTouchSocket/UnitTestTouchSocket.csproj new file mode 100644 index 00000000..91bee258 --- /dev/null +++ b/test/UnitTestTouchSocket/UnitTestTouchSocket.csproj @@ -0,0 +1,13 @@ + + + + + + + + + + + + + From 9d2bea2bdbc26b98713710bb3a096d1119a9944d Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 27 Jun 2025 15:04:25 +0800 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20=E6=A0=B9=E6=8D=AE=E6=9C=80?= =?UTF-8?q?=E6=96=B0=E6=8E=A5=E5=8F=A3=E6=9B=B4=E6=96=B0=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BootstrapBlazor.TouchSocket.csproj | 6 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../Services/DefaultTcpSocketClient.cs | 220 ++++++++++++------ .../Services/DefaultTcpSocketFactory.cs | 8 +- 4 files changed, 152 insertions(+), 84 deletions(-) diff --git a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj index 4368fd49..a735dd48 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj +++ b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj @@ -18,11 +18,7 @@ - - - - - + diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs b/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs index 54c948ed..aa04a612 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.DependencyInjection; public static class BootstrapBlazorTouchSocketServiceExtensions { /// - /// 添加 AzureOpenAIService 服务 + /// 添加 TouchSocket 服务 /// /// public static IServiceCollection AddBootstrapBlazorTouchSocketService(this IServiceCollection services) diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs index 448aabe4..46bf1328 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs @@ -10,53 +10,75 @@ namespace BootstrapBlazor.Components; -sealed class DefaultTcpSocketClient(IPEndPoint endPoint) : ITcpSocketClient +sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : TcpSocketClientBase { private TcpClient? _client; - private IDataPackageHandler? _dataPackageHandler; private CancellationTokenSource? _receiveCancellationTokenSource; private IPEndPoint? _remoteEndPoint; - public bool IsConnected => _client?.Online ?? false; + public override bool IsConnected => _client?.Online ?? false; - public IPEndPoint LocalEndPoint { get; set; } = endPoint; + private IReceiver? _receiver; [NotNull] public ILogger? Logger { get; set; } - public int ReceiveBufferSize { get; set; } = 1024 * 10; - - public Func, ValueTask>? ReceivedCallBack { get; set; } - - public 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 CloseAsync(); - // 创建新的 TouchSocket TcpClient 实例 + // 创建新的 TcpClient 实例 _client ??= new TcpClient(); - await _client.SetupAsync(new TouchSocketConfig().SetBindIPHost(new IPHost(LocalEndPoint.Address, LocalEndPoint.Port))); + _remoteEndPoint = endPoint; - await _client.ConnectAsync(new IPHost(endPoint.Address, endPoint.Port), ); + // 设置本地端点 + var config = new TouchSocketConfig() + .SetBindIPHost(new IPHost(localEndPoint.Address, localEndPoint.Port)) + .SetRemoteIPHost(new IPHost(endPoint.Address, endPoint.Port)) + .SetMaxBufferSize(ReceiveBufferSize) + .SetMinBufferSize(ReceiveBufferSize / 10); + await _client.SetupAsync(config); - // 开始接收数据 - //_ = Task.Run(ReceiveAsync, token); + var connectTimeout = ConnectTimeout == 0 ? int.MaxValue : ConnectTimeout; + await _client.ConnectAsync(connectTimeout, token); - //LocalEndPoint = (IPEndPoint)_client.Client.LocalEndPoint!; - _remoteEndPoint = endPoint; + if (IsConnected) + { + // 设置本地以及远端端点信息 + if (_client.MainSocket.LocalEndPoint is IPEndPoint local) + { + LocalEndPoint = local; + } + + _receiver = _client.CreateReceiver(); + _receiver.CacheMode = true; + _receiver.MaxCacheSize = ReceiveBufferSize; + + if (_client.MainSocket.RemoteEndPoint is IPEndPoint remote) + { + _remoteEndPoint = remote; + } + if (IsAutoReceive) + { + _ = Task.Run(AutoReceiveAsync); + } + } ret = true; } catch (OperationCanceledException ex) { - Logger.LogWarning(ex, "TCP Socket connect operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint); + if (token.IsCancellationRequested) + { + Logger.LogWarning(ex, "TCP Socket connect operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint); + } + else + { + Logger.LogWarning(ex, "TCP Socket connect operation timed out from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint); + } } catch (Exception ex) { @@ -65,7 +87,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 { Online: true }) { @@ -75,17 +97,32 @@ public async ValueTask SendAsync(ReadOnlyMemory data, CancellationTo var ret = false; try { - if (_dataPackageHandler != null) + var sendToken = token; + if (SendTimeout > 0) + { + // 设置发送超时时间 + var sendTokenSource = new CancellationTokenSource(SendTimeout); + sendToken = CancellationTokenSource.CreateLinkedTokenSource(token, sendTokenSource.Token).Token; + } + + if (DataPackageHandler != null) { - data = await _dataPackageHandler.SendAsync(data); + data = await DataPackageHandler.SendAsync(data, sendToken); } - //var stream = _client.GetStream(); - //await stream.WriteAsync(data, token); + + await _client.SendAsync(data).WaitAsync(sendToken); ret = true; } catch (OperationCanceledException ex) { - Logger.LogWarning(ex, "TCP Socket send operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); + if (token.IsCancellationRequested) + { + Logger.LogWarning(ex, "TCP Socket send operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); + } + else + { + Logger.LogWarning(ex, "TCP Socket send operation timed out from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); + } } catch (Exception ex) { @@ -94,7 +131,25 @@ public async ValueTask SendAsync(ReadOnlyMemory data, CancellationTo return ret; } - private async ValueTask ReceiveAsync() + public override async ValueTask> ReceiveAsync(CancellationToken token = default) + { + if (_client is not { Online: true }) + { + throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); + } + + if (IsAutoReceive) + { + throw new InvalidOperationException("Cannot call ReceiveAsync when IsAutoReceive is true. Use the auto-receive mechanism instead."); + } + + using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); + var buffer = block.Memory; + var len = await ReceiveCoreAsync(buffer, token); + return buffer[0..len]; + } + + private async ValueTask AutoReceiveAsync() { _receiveCancellationTokenSource ??= new(); while (_receiveCancellationTokenSource is { IsCancellationRequested: false }) @@ -104,78 +159,95 @@ private async ValueTask ReceiveAsync() throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); } - try + using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); + var buffer = block.Memory; + var len = await ReceiveCoreAsync(buffer, _receiveCancellationTokenSource.Token); + if (len == 0) { - 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]; + break; + } + } + } - // if (ReceivedCallBack != null) - // { - // await ReceivedCallBack(buffer); - // } + private async ValueTask ReceiveCoreAsync(Memory buffer, CancellationToken token) + { + var len = 0; + try + { + var receiveToken = token; + if (ReceiveTimeout > 0) + { + // 设置接收超时时间 + var receiveTokenSource = new CancellationTokenSource(ReceiveTimeout); + receiveToken = CancellationTokenSource.CreateLinkedTokenSource(receiveToken, receiveTokenSource.Token).Token; + } - // if (_dataPackageHandler != null) - // { - // await _dataPackageHandler.ReceiveAsync(buffer); - // } - //} + using var result = await _receiver!.ReadAsync(receiveToken); + if (result.IsCompleted) + { + Logger.LogInformation("TCP Socket {LocalEndPoint} received 0 data closed by {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); + return 0; } - catch (OperationCanceledException ex) + + result.ByteBlock.Memory.CopyTo(buffer); + len = result.ByteBlock.Length; + buffer = buffer[..len]; + + if (ReceivedCallBack != null) { - Logger.LogWarning(ex, "TCP Socket receive operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); + await ReceivedCallBack(buffer); } - catch (Exception ex) + + if (DataPackageHandler != null) { - Logger.LogError(ex, "TCP Socket receive failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); + await DataPackageHandler.ReceiveAsync(buffer, receiveToken); + result.ByteBlock.Seek(len); } } + catch (OperationCanceledException ex) + { + if (token.IsCancellationRequested) + { + Logger.LogWarning(ex, "TCP Socket receive operation canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); + } + else + { + Logger.LogWarning(ex, "TCP Socket receive operation timed out from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "TCP Socket receive failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); + } + return len; } - public void Close() - { - Dispose(true); - } - - private void Dispose(bool disposing) + protected override async ValueTask DisposeAsync(bool disposing) { if (disposing) { _remoteEndPoint = null; // 取消接收数据的任务 - if (_receiveCancellationTokenSource is not null) + if (_receiveCancellationTokenSource != null) { _receiveCancellationTokenSource.Cancel(); _receiveCancellationTokenSource.Dispose(); _receiveCancellationTokenSource = null; } + if (_receiver != null) + { + _receiver.Dispose(); + _receiver = null; + } + // 释放 TcpClient 资源 if (_client != null) { - _client.CloseAsync(); + await _client.CloseAsync(); _client = null; } } } - - /// - /// - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } } diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs index 0aec4429..290e1800 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs @@ -38,14 +38,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(); } @@ -54,9 +54,9 @@ private void Dispose(bool disposing) /// /// /// - public void Dispose() + public async ValueTask DisposeAsync() { - Dispose(true); + await DisposeAsync(true); GC.SuppressFinalize(this); } } From ab09440d1b8d782e81b055ceb6ae2294cad66709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=A5=E6=B1=9D=E6=A3=8B=E8=8C=97?= <505554090@qq.com> Date: Sat, 28 Jun 2025 09:35:16 +0800 Subject: [PATCH 12/17] =?UTF-8?q?refactor(DefaultTcpSocketClient):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20SendAsync=20=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 DefaultTcpSocketClient 类中,修改 SendAsync 方法的调用方式,去掉 WaitAsync,直接使用 SendAsync(data, sendToken),使代码更简洁并提高异步操作效率 --- .../Services/DefaultTcpSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs index 46bf1328..002451df 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs @@ -110,7 +110,7 @@ public override async ValueTask SendAsync(ReadOnlyMemory data, Cance data = await DataPackageHandler.SendAsync(data, sendToken); } - await _client.SendAsync(data).WaitAsync(sendToken); + await _client.SendAsync(data,sendToken); ret = true; } catch (OperationCanceledException ex) From 1a648581697310d466b402072069e3fdf9c85e30 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 28 Jun 2025 23:25:28 +0800 Subject: [PATCH 13/17] =?UTF-8?q?refactor:=20=E7=B2=BE=E7=AE=80=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=9B=B4=E6=96=B0=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BootstrapBlazor.Extensions.sln | 7 + .../BootstrapBlazor.TouchSocket.csproj | 8 +- .../Extensions/ServiceCollectionExtensions.cs | 5 +- .../Services/DefaultTcpSocketClient.cs | 253 ------------------ .../Services/DefaultTcpSocketFactory.cs | 62 ----- .../Services/DefaultTcpSocketProvider.cs | 84 ++++++ .../TcpSocketFactoryTest.cs | 153 +++++++---- 7 files changed, 194 insertions(+), 378 deletions(-) delete mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs delete mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs create mode 100644 src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketProvider.cs diff --git a/BootstrapBlazor.Extensions.sln b/BootstrapBlazor.Extensions.sln index c91fe4d9..cefdf27b 100644 --- a/BootstrapBlazor.Extensions.sln +++ b/BootstrapBlazor.Extensions.sln @@ -198,6 +198,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.OfficeViewe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestTouchSocket", "test\UnitTestTouchSocket\UnitTestTouchSocket.csproj", "{23D21A45-64A3-704C-27C0-73399048ED03}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor", "..\BootstrapBlazor\src\BootstrapBlazor\BootstrapBlazor.csproj", "{C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -540,6 +542,10 @@ Global {23D21A45-64A3-704C-27C0-73399048ED03}.Debug|Any CPU.Build.0 = Debug|Any CPU {23D21A45-64A3-704C-27C0-73399048ED03}.Release|Any CPU.ActiveCfg = Release|Any CPU {23D21A45-64A3-704C-27C0-73399048ED03}.Release|Any CPU.Build.0 = Release|Any CPU + {C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -633,6 +639,7 @@ Global {FD23CEA1-78EB-85D7-8EDF-047657355B52} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} {2436940C-5920-D801-8A81-721F4C20A355} = {FF1089BE-C704-4374-B629-C57C08E1798F} {23D21A45-64A3-704C-27C0-73399048ED03} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} + {C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF} diff --git a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj index a735dd48..fc55b108 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj +++ b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj @@ -14,11 +14,15 @@ - + - + + + + + diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs b/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs index aa04a612..b3632f24 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs @@ -15,11 +15,10 @@ public static class BootstrapBlazorTouchSocketServiceExtensions /// 添加 TouchSocket 服务 /// /// - public static IServiceCollection AddBootstrapBlazorTouchSocketService(this IServiceCollection services) + public static IServiceCollection AddBootstrapBlazorTouchSocketProviderService(this IServiceCollection services) { - services.AddSingleton(); + services.AddTransient(); - // TBD: 这里注入 TouchSocket 相关服务 return services; } } diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs deleted file mode 100644 index 002451df..00000000 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketClient.cs +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Website: https://www.blazor.zone or https://argozhang.github.io/ - -using Microsoft.Extensions.Logging; -using System.Buffers; -using System.Net; -using TouchSocket.Core; -using TouchSocket.Sockets; - -namespace BootstrapBlazor.Components; - -sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : TcpSocketClientBase -{ - private TcpClient? _client; - private CancellationTokenSource? _receiveCancellationTokenSource; - private IPEndPoint? _remoteEndPoint; - - public override bool IsConnected => _client?.Online ?? false; - - private IReceiver? _receiver; - - [NotNull] - public ILogger? Logger { get; set; } - - public override async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) - { - var ret = false; - try - { - // 释放资源 - await CloseAsync(); - - // 创建新的 TcpClient 实例 - _client ??= new TcpClient(); - _remoteEndPoint = endPoint; - - // 设置本地端点 - var config = new TouchSocketConfig() - .SetBindIPHost(new IPHost(localEndPoint.Address, localEndPoint.Port)) - .SetRemoteIPHost(new IPHost(endPoint.Address, endPoint.Port)) - .SetMaxBufferSize(ReceiveBufferSize) - .SetMinBufferSize(ReceiveBufferSize / 10); - await _client.SetupAsync(config); - - var connectTimeout = ConnectTimeout == 0 ? int.MaxValue : ConnectTimeout; - await _client.ConnectAsync(connectTimeout, token); - - if (IsConnected) - { - // 设置本地以及远端端点信息 - if (_client.MainSocket.LocalEndPoint is IPEndPoint local) - { - LocalEndPoint = local; - } - - _receiver = _client.CreateReceiver(); - _receiver.CacheMode = true; - _receiver.MaxCacheSize = ReceiveBufferSize; - - if (_client.MainSocket.RemoteEndPoint is IPEndPoint remote) - { - _remoteEndPoint = remote; - } - if (IsAutoReceive) - { - _ = Task.Run(AutoReceiveAsync); - } - } - ret = true; - } - catch (OperationCanceledException ex) - { - if (token.IsCancellationRequested) - { - Logger.LogWarning(ex, "TCP Socket connect operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint); - } - else - { - Logger.LogWarning(ex, "TCP Socket connect operation timed out 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 override async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) - { - if (_client is not { Online: true }) - { - throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); - } - - var ret = false; - try - { - var sendToken = token; - if (SendTimeout > 0) - { - // 设置发送超时时间 - var sendTokenSource = new CancellationTokenSource(SendTimeout); - sendToken = CancellationTokenSource.CreateLinkedTokenSource(token, sendTokenSource.Token).Token; - } - - if (DataPackageHandler != null) - { - data = await DataPackageHandler.SendAsync(data, sendToken); - } - - await _client.SendAsync(data,sendToken); - ret = true; - } - catch (OperationCanceledException ex) - { - if (token.IsCancellationRequested) - { - Logger.LogWarning(ex, "TCP Socket send operation was canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); - } - else - { - Logger.LogWarning(ex, "TCP Socket send operation timed out from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "TCP Socket send failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); - } - return ret; - } - - public override async ValueTask> ReceiveAsync(CancellationToken token = default) - { - if (_client is not { Online: true }) - { - throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); - } - - if (IsAutoReceive) - { - throw new InvalidOperationException("Cannot call ReceiveAsync when IsAutoReceive is true. Use the auto-receive mechanism instead."); - } - - using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); - var buffer = block.Memory; - var len = await ReceiveCoreAsync(buffer, token); - return buffer[0..len]; - } - - private async ValueTask AutoReceiveAsync() - { - _receiveCancellationTokenSource ??= new(); - while (_receiveCancellationTokenSource is { IsCancellationRequested: false }) - { - if (_client is not { Online: true }) - { - throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); - } - - using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); - var buffer = block.Memory; - var len = await ReceiveCoreAsync(buffer, _receiveCancellationTokenSource.Token); - if (len == 0) - { - break; - } - } - } - - private async ValueTask ReceiveCoreAsync(Memory buffer, CancellationToken token) - { - var len = 0; - try - { - var receiveToken = token; - if (ReceiveTimeout > 0) - { - // 设置接收超时时间 - var receiveTokenSource = new CancellationTokenSource(ReceiveTimeout); - receiveToken = CancellationTokenSource.CreateLinkedTokenSource(receiveToken, receiveTokenSource.Token).Token; - } - - using var result = await _receiver!.ReadAsync(receiveToken); - if (result.IsCompleted) - { - Logger.LogInformation("TCP Socket {LocalEndPoint} received 0 data closed by {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); - return 0; - } - - result.ByteBlock.Memory.CopyTo(buffer); - len = result.ByteBlock.Length; - buffer = buffer[..len]; - - if (ReceivedCallBack != null) - { - await ReceivedCallBack(buffer); - } - - if (DataPackageHandler != null) - { - await DataPackageHandler.ReceiveAsync(buffer, receiveToken); - result.ByteBlock.Seek(len); - } - } - catch (OperationCanceledException ex) - { - if (token.IsCancellationRequested) - { - Logger.LogWarning(ex, "TCP Socket receive operation canceled from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); - } - else - { - Logger.LogWarning(ex, "TCP Socket receive operation timed out from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "TCP Socket receive failed from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); - } - return len; - } - - protected override async ValueTask DisposeAsync(bool disposing) - { - if (disposing) - { - _remoteEndPoint = null; - - // 取消接收数据的任务 - if (_receiveCancellationTokenSource != null) - { - _receiveCancellationTokenSource.Cancel(); - _receiveCancellationTokenSource.Dispose(); - _receiveCancellationTokenSource = null; - } - - if (_receiver != null) - { - _receiver.Dispose(); - _receiver = null; - } - - // 释放 TcpClient 资源 - if (_client != null) - { - await _client.CloseAsync(); - _client = null; - } - } - } -} diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs deleted file mode 100644 index 290e1800..00000000 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketFactory.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Website: https://www.blazor.zone or https://argozhang.github.io/ - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System.Collections.Concurrent; -using System.Net; -using System.Runtime.Versioning; - -namespace BootstrapBlazor.Components; - -[UnsupportedOSPlatform("browser")] -sealed class DefaultTcpSocketFactory(IServiceProvider provider) : ITcpSocketFactory -{ - private readonly ConcurrentDictionary _pool = new(); - - public ITcpSocketClient GetOrCreate(string name, Func valueFactory) - { - return _pool.GetOrAdd(name, key => - { - var endPoint = valueFactory(key); - var client = new DefaultTcpSocketClient(endPoint) - { - Logger = provider.GetService>() - }; - return client; - }); - } - - public ITcpSocketClient? Remove(string name) - { - ITcpSocketClient? client = null; - if (_pool.TryRemove(name, out var c)) - { - client = c; - } - return client; - } - - private async ValueTask DisposeAsync(bool disposing) - { - if (disposing) - { - // 释放托管资源 - foreach (var socket in _pool.Values) - { - await socket.DisposeAsync(); - } - _pool.Clear(); - } - } - - /// - /// - /// - public async ValueTask DisposeAsync() - { - await DisposeAsync(true); - GC.SuppressFinalize(this); - } -} diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketProvider.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketProvider.cs new file mode 100644 index 00000000..40d7972c --- /dev/null +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketProvider.cs @@ -0,0 +1,84 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using System.Buffers; +using System.IO.Pipelines; +using System.Net; +using TouchSocket.Core; +using TouchSocket.Sockets; + +namespace BootstrapBlazor.Components; + +sealed class DefaultTcpSocketProvider : TcpClientBase, ISocketClientProvider +{ + private readonly Pipe _pipe = new(); + + /// + /// + /// + public bool IsConnected => Online; + + /// + /// + /// + public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); + + /// + /// + /// + public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) + { + await SetupAsync(new TouchSocketConfig() + .SetBindIPHost(new IPHost(LocalEndPoint.Address, LocalEndPoint.Port)) + .SetRemoteIPHost(new IPHost(endPoint.Address, endPoint.Port))); + await TcpConnectAsync(int.MaxValue, token); + if (Online) + { + if (MainSocket.LocalEndPoint is IPEndPoint localEndPoint) + { + LocalEndPoint = localEndPoint; + } + } + return Online; + } + + /// + /// + /// + public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + await ProtectedDefaultSendAsync(data, token); + return true; + } + + /// + /// + /// + public async ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default) + { + var result = await _pipe.Reader.ReadAsync(token); + if (result.IsCompleted) + { + return 0; + } + + result.Buffer.CopyTo(buffer.Span); + return (int)result.Buffer.Length; + } + + protected override async ValueTask OnTcpReceiving(ByteBlock byteBlock) + { + await _pipe.Writer.WriteAsync(byteBlock.Memory); + await _pipe.Writer.FlushAsync(); + return true; + } + + /// + /// + /// + public async ValueTask CloseAsync() + { + await CloseAsync(string.Empty); + } +} diff --git a/test/UnitTestTouchSocket/TcpSocketFactoryTest.cs b/test/UnitTestTouchSocket/TcpSocketFactoryTest.cs index b85ef674..2bc39c0a 100644 --- a/test/UnitTestTouchSocket/TcpSocketFactoryTest.cs +++ b/test/UnitTestTouchSocket/TcpSocketFactoryTest.cs @@ -1,16 +1,14 @@ -// 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 +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ using Microsoft.Extensions.Logging; using System.Net; using System.Net.Sockets; +using System.Reflection; using System.Text; -[assembly: CollectionBehavior(DisableTestParallelization = true)] - -namespace UnitTestTouchSocket; +namespace UnitTest.Services; public class TcpSocketFactoryTest { @@ -23,20 +21,22 @@ public async Task GetOrCreate_Ok() { builder.AddProvider(new MockLoggerProvider()); }); - sc.AddBootstrapBlazorTouchSocketService(); + sc.AddBootstrapBlazorTcpSocketFactory(); + sc.AddBootstrapBlazorTouchSocketProviderService(); + var provider = sc.BuildServiceProvider(); var factory = provider.GetRequiredService(); - var client1 = factory.GetOrCreate("demo", key => Utility.ConvertToIpEndPoint("localhost", 0)); + var client1 = factory.GetOrCreate("demo", op => op.LocalEndPoint = Utility.ConvertToIpEndPoint("localhost", 0)); await client1.CloseAsync(); - var client2 = factory.GetOrCreate("demo", key => Utility.ConvertToIpEndPoint("localhost", 0)); + var client2 = factory.GetOrCreate("demo", op => op.LocalEndPoint = Utility.ConvertToIpEndPoint("localhost", 0)); Assert.Equal(client1, client2); var ip = Dns.GetHostAddresses(Dns.GetHostName(), AddressFamily.InterNetwork).FirstOrDefault() ?? IPAddress.Loopback; - var client3 = factory.GetOrCreate("demo1", key => Utility.ConvertToIpEndPoint(ip.ToString(), 0)); + var client3 = factory.GetOrCreate("demo1", op => op.LocalEndPoint = Utility.ConvertToIpEndPoint(ip.ToString(), 0)); // 测试不合格 IP 地址 - var client4 = factory.GetOrCreate("demo2", key => Utility.ConvertToIpEndPoint("256.0.0.1", 0)); + var client4 = factory.GetOrCreate("demo2", op => op.LocalEndPoint = Utility.ConvertToIpEndPoint("256.0.0.1", 0)); var client5 = factory.Remove("demo2"); Assert.Equal(client4, client5); @@ -50,7 +50,7 @@ public async Task GetOrCreate_Ok() public async Task ConnectAsync_Timeout() { var client = CreateClient(); - client.ConnectTimeout = 1000; + client.Options.ConnectTimeout = 100; var connect = await client.ConnectAsync("localhost", 9999); Assert.False(connect); @@ -78,6 +78,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() { @@ -85,7 +105,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); @@ -104,6 +124,17 @@ public async Task SendAsync_Error() var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); var ex = await Assert.ThrowsAsync(async () => await client.SendAsync(data)); Assert.NotNull(ex); + + // 测试发送失败 + var port = 8892; + var server = StartTcpServer(port, MockSplitPackageAsync); + + client.SetDataHandler(new MockSendErrorHandler()); + await client.ConnectAsync("localhost", port); + Assert.True(client.IsConnected); + + // 内部生成异常日志 + await client.SendAsync(data); } [Fact] @@ -112,7 +143,7 @@ public async Task SendAsync_Cancel() var port = 8881; var server = StartTcpServer(port, MockSplitPackageAsync); - var client = CreateClient(nameof(SendAsync_Cancel)); + var client = CreateClient(); Assert.False(client.IsConnected); // 连接 TCP Server @@ -128,7 +159,7 @@ public async Task SendAsync_Cancel() // 设置延时发送适配器 // 延时发送期间关闭 Socket 连接导致内部报错 - client.SetDataHandler(new MockSendErrorHandler() + client.SetDataHandler(new MockSendCancelHandler() { Socket = client }); @@ -156,7 +187,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); @@ -172,7 +203,7 @@ public async Task ReceiveAsync_Cancel() var port = 8889; var server = StartTcpServer(port, MockSplitPackageAsync); - var client = CreateClient(nameof(ReceiveAsync_Cancel)); + var client = CreateClient(); client.SetDataHandler(new MockReceiveTimeoutHandler()); await client.ConnectAsync("localhost", port); @@ -181,7 +212,10 @@ public async Task ReceiveAsync_Cancel() await client.SendAsync(data); // 通过反射取消令牌 - var fieldInfo = client.GetType().GetField("_receiveCancellationTokenSource", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var baseType = client.GetType().BaseType; + Assert.NotNull(baseType); + + var fieldInfo = baseType.GetField("_receiveCancellationTokenSource", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(fieldInfo); var tokenSource = fieldInfo.GetValue(client) as CancellationTokenSource; Assert.NotNull(tokenSource); @@ -192,36 +226,21 @@ public async Task ReceiveAsync_Cancel() [Fact] public async Task ReceiveAsync_InvalidOperationException() { - var port = 8890; - var server = StartTcpServer(port, MockSplitPackageAsync); - + // 未连接时调用 ReceiveAsync 方法会抛出 InvalidOperationException 异常 var client = CreateClient(); - - //未连接 var ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); Assert.NotNull(ex); - // 反射给 _client 赋值但是未连接 - var fieldInfo = client.GetType().GetField("_client", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.NotNull(fieldInfo); - fieldInfo.SetValue(client, new TouchSocket.Sockets.TcpClient()); - ex = null; - ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); - Assert.NotNull(ex); - - await client.ConnectAsync("localhost", port); - ex = null; - ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); + // 已连接但是启用了自动接收功能时调用 ReceiveAsync 方法会抛出 InvalidOperationException 异常 + var port = 8893; + var server = StartTcpServer(port, MockSplitPackageAsync); - await client.CloseAsync(); - client.IsAutoReceive = false; + client.Options.IsAutoReceive = true; var connected = await client.ConnectAsync("localhost", port); Assert.True(connected); - var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); - await client.SendAsync(data); - var payload = await client.ReceiveAsync(); - Assert.Equal(payload.ToArray(), [1, 2, 3, 4, 5]); + ex = await Assert.ThrowsAsync(async () => await client.ReceiveAsync()); + Assert.NotNull(ex); } [Fact] @@ -231,10 +250,14 @@ public async Task ReceiveAsync_Ok() var server = StartTcpServer(port, MockSplitPackageAsync); var client = CreateClient(); - client.IsAutoReceive = false; - await client.ConnectAsync("localhost", port); + client.Options.IsAutoReceive = false; + var connected = await client.ConnectAsync("localhost", port); + Assert.True(connected); + var data = new ReadOnlyMemory([1, 2, 3, 4, 5]); - await client.SendAsync(data); + var send = await client.SendAsync(data); + Assert.True(send); + var payload = await client.ReceiveAsync(); Assert.Equal(payload.ToArray(), [1, 2, 3, 4, 5]); } @@ -245,7 +268,10 @@ public async Task ReceiveAsync_Error() var client = CreateClient(); // 测试未建立连接前调用 ReceiveAsync 方法报异常逻辑 - var methodInfo = client.GetType().GetMethod("AutoReceiveAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var baseType = client.GetType().BaseType; + Assert.NotNull(baseType); + + var methodInfo = baseType.GetMethod("AutoReceiveAsync", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(methodInfo); var task = (ValueTask)methodInfo.Invoke(client, null)!; @@ -255,10 +281,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()); @@ -289,7 +315,7 @@ public async Task ReceiveAsync_Error() [Fact] public async Task CloseByRemote_Ok() { - var client = CreateClient(nameof(CloseByRemote_Ok)); + var client = CreateClient(); var port = 8883; var server = StartTcpServer(port, MockAutoClosePackageAsync); @@ -411,7 +437,7 @@ public async Task DelimiterDataPackageHandler_Ok() var connect = await client.ConnectAsync("localhost", port); var tcs = new TaskCompletionSource(); - var receivedBuffer = ReadOnlyMemory.Empty; + ReadOnlyMemory receivedBuffer = ReadOnlyMemory.Empty; // 增加数据库处理适配器 client.SetDataHandler(new DelimiterDataPackageHandler([0x13, 0x10]) @@ -476,7 +502,7 @@ private static async Task MockDelimiterPackageAsync(TcpClient client) using var stream = client.GetStream(); while (true) { - var buffer = new byte[1024]; + var buffer = new byte[10240]; var len = await stream.ReadAsync(buffer); if (len == 0) { @@ -499,7 +525,7 @@ private static async Task MockSplitPackageAsync(TcpClient client) using var stream = client.GetStream(); while (true) { - var buffer = new byte[10240]; + var buffer = new byte[1024]; var len = await stream.ReadAsync(buffer); if (len == 0) { @@ -548,11 +574,10 @@ private static async Task MockStickyPackageAsync(TcpClient client) } } - private static async Task MockAutoClosePackageAsync(TcpClient client) + private static Task MockAutoClosePackageAsync(TcpClient client) { - // 增加延时防止 Send 方法抛异常 - await Task.Delay(10); client.Close(); + return Task.CompletedTask; } private static void StopTcpServer(TcpListener server) @@ -560,17 +585,19 @@ private static void StopTcpServer(TcpListener server) server?.Stop(); } - private static ITcpSocketClient CreateClient(string name = "test") + private static ITcpSocketClient CreateClient() { var sc = new ServiceCollection(); sc.AddLogging(builder => { builder.AddProvider(new MockLoggerProvider()); }); - sc.AddBootstrapBlazorTouchSocketService(); + sc.AddBootstrapBlazorTcpSocketFactory(); + sc.AddBootstrapBlazorTouchSocketProviderService(); + var provider = sc.BuildServiceProvider(); var factory = provider.GetRequiredService(); - var client = factory.GetOrCreate(name, key => Utility.ConvertToIpEndPoint("localhost", 0)); + var client = factory.GetOrCreate("test", op => op.LocalEndPoint = Utility.ConvertToIpEndPoint("localhost", 0)); return client; } @@ -609,6 +636,16 @@ 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) From d69197956509824e27ffc4db0736e55b471e916b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=A5=E6=B1=9D=E6=A3=8B=E8=8C=97?= <505554090@qq.com> Date: Sun, 20 Jul 2025 16:26:34 +0800 Subject: [PATCH 14/17] =?UTF-8?q?=E9=87=8D=E6=9E=84(tcp):=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0TouchSocket=E7=89=88=E6=9C=AC=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=BF=9E=E6=8E=A5=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `BootstrapBlazor.TouchSocket.csproj` 文件中,将 `TouchSocket` 的版本从 `3.1.11.8` 更新为 `4.0.0-Alpha.10`。 在 `DefaultTcpSocketProvider.cs` 文件中,类的访问修饰符从 `sealed` 改为 `internal`,并添加了对 `System.Diagnostics` 的引用。 修改 `IsConnected` 属性的实现,确保调用基类的 `Online` 属性。 添加新的 `CloseAsync` 方法,重写连接逻辑以包含异常处理,并在连接成功后进行调试断言。 更新 `ReceiveAsync` 方法的数据读取逻辑,使用基类的 `Transport.Input` 进行数据读取,并确保正确处理数据缓冲区。 重写 `SendAsync` 方法,添加对取消令牌的处理,并确保在发送数据时正确管理锁。 重写 `ReceiveLoopAsync` 方法,注释说明新的接收循环逻辑,确保数据通过管道直接读取,而不进行额外的数据读取操作 --- .../BootstrapBlazor.TouchSocket.csproj | 2 +- .../Services/DefaultTcpSocketProvider.cs | 93 +++++++++++++------ 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj index fc55b108..5c67b9bc 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj +++ b/src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketProvider.cs b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketProvider.cs index 40d7972c..13065233 100644 --- a/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketProvider.cs +++ b/src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketProvider.cs @@ -3,6 +3,7 @@ // Website: https://www.blazor.zone or https://argozhang.github.io/ using System.Buffers; +using System.Diagnostics; using System.IO.Pipelines; using System.Net; using TouchSocket.Core; @@ -10,20 +11,26 @@ namespace BootstrapBlazor.Components; -sealed class DefaultTcpSocketProvider : TcpClientBase, ISocketClientProvider +internal sealed class DefaultTcpSocketProvider : TcpClientBase, ISocketClientProvider { - private readonly Pipe _pipe = new(); - /// /// /// - public bool IsConnected => Online; + public bool IsConnected => base.Online; /// /// /// public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); + /// + /// + /// + public async ValueTask CloseAsync() + { + await base.CloseAsync(string.Empty); + } + /// /// /// @@ -32,24 +39,23 @@ public async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken await SetupAsync(new TouchSocketConfig() .SetBindIPHost(new IPHost(LocalEndPoint.Address, LocalEndPoint.Port)) .SetRemoteIPHost(new IPHost(endPoint.Address, endPoint.Port))); - await TcpConnectAsync(int.MaxValue, token); - if (Online) + + try { + await TcpConnectAsync(int.MaxValue, token); + Debug.Assert(MainSocket != null, "MainSocket cannot be null after connection."); + Debug.Assert(base.Online, "Online should be true after successful connection."); if (MainSocket.LocalEndPoint is IPEndPoint localEndPoint) { LocalEndPoint = localEndPoint; } + return true; + } + catch (Exception ex) + { + this.Logger?.Exception(this, ex); + return false; } - return Online; - } - - /// - /// - /// - public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) - { - await ProtectedDefaultSendAsync(data, token); - return true; } /// @@ -57,28 +63,61 @@ public async ValueTask SendAsync(ReadOnlyMemory data, CancellationTo /// public async ValueTask ReceiveAsync(Memory buffer, CancellationToken token = default) { - var result = await _pipe.Reader.ReadAsync(token); + token.ThrowIfCancellationRequested(); + this.ThrowIfTcpClientNotConnected(); + this.ThrowIfDisposed(); + + var result = await base.Transport.Input.ReadAsync(token); if (result.IsCompleted) { return 0; } + var length = (int)Math.Min(result.Buffer.Length, buffer.Length); - result.Buffer.CopyTo(buffer.Span); - return (int)result.Buffer.Length; - } + var sequence = result.Buffer.Slice(0, length); - protected override async ValueTask OnTcpReceiving(ByteBlock byteBlock) - { - await _pipe.Writer.WriteAsync(byteBlock.Memory); - await _pipe.Writer.FlushAsync(); - return true; + sequence.CopyTo(buffer.Span); + base.Transport.Input.AdvanceTo(sequence.End); + return length; } /// /// /// - public async ValueTask CloseAsync() + public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + base.ThrowIfTcpClientNotConnected(); + base.ThrowIfDisposed(); + var pipeWriter = base.Transport.Output; + var locker = base.Transport.SemaphoreSlimForWriter; + await locker.WaitAsync(token); + try + { + pipeWriter.Write(data.Span); + var result = await pipeWriter.FlushAsync(token); + if (result.IsCanceled || result.IsCompleted) + { + return false; + } + return true; + } + catch (Exception ex) + { + this.Logger?.Exception(this, ex); + return false; + } + finally + { + locker.Release(); + } + } + + protected override sealed async Task ReceiveLoopAsync(ITransport transport) { - await CloseAsync(string.Empty); + //重写接收循环方法 + //此处不做任何数据读取 + //让数据直接到ReceiveAsync使用管道直接读取数据 + await Task.Delay(-1, transport.ClosedToken); } } From 681d822e65d65c90d1cebd628187efab28982a38 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 21 Jul 2025 13:15:51 +0800 Subject: [PATCH 15/17] =?UTF-8?q?chore:=20=E6=81=A2=E5=A4=8D=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BootstrapBlazor.Extensions.sln | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/BootstrapBlazor.Extensions.sln b/BootstrapBlazor.Extensions.sln index d8b048bf..99437ae7 100644 --- a/BootstrapBlazor.Extensions.sln +++ b/BootstrapBlazor.Extensions.sln @@ -194,6 +194,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestTouchSocket", "test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor", "..\BootstrapBlazor\src\BootstrapBlazor\BootstrapBlazor.csproj", "{C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.TouchSocket", "src\extensions\BootstrapBlazor.TouchSocket\BootstrapBlazor.TouchSocket.csproj", "{FD23CEA1-78EB-85D7-8EDF-047657355B52}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -528,6 +530,10 @@ Global {C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040}.Release|Any CPU.Build.0 = Release|Any CPU + {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD23CEA1-78EB-85D7-8EDF-047657355B52}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -619,6 +625,7 @@ Global {2436940C-5920-D801-8A81-721F4C20A355} = {FF1089BE-C704-4374-B629-C57C08E1798F} {23D21A45-64A3-704C-27C0-73399048ED03} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} {C8C8F286-4D4C-C5F9-6ADD-C1BCE188E040} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} + {FD23CEA1-78EB-85D7-8EDF-047657355B52} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF} From e0cf878c411c71ed6df13e1f08c80f77e9fdcaff Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 5 Aug 2025 15:00:31 +0800 Subject: [PATCH 16/17] chore: bump version 9.0.0 --- .../BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj | 2 +- .../BootstrapBlazor.TcpSocket/BootstrapBlazor.TcpSocket.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj b/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj index 284ada07..940fb5fe 100644 --- a/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj +++ b/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj @@ -1,7 +1,7 @@  - 9.0.0-beta01 + 9.0.0 diff --git a/src/extensions/BootstrapBlazor.TcpSocket/BootstrapBlazor.TcpSocket.csproj b/src/extensions/BootstrapBlazor.TcpSocket/BootstrapBlazor.TcpSocket.csproj index 00d1631a..33f5d97a 100644 --- a/src/extensions/BootstrapBlazor.TcpSocket/BootstrapBlazor.TcpSocket.csproj +++ b/src/extensions/BootstrapBlazor.TcpSocket/BootstrapBlazor.TcpSocket.csproj @@ -1,7 +1,7 @@  - 9.0.0-beta01 + 9.0.0 From 5cb3907d0b26816d6a698112a50eecc9640d94a0 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 5 Aug 2025 15:03:05 +0800 Subject: [PATCH 17/17] =?UTF-8?q?revert:=20=E6=92=A4=E9=94=80=E5=85=B6?= =?UTF-8?q?=E4=BD=99=E9=A1=B9=E7=9B=AE=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ootstrapBlazor.OfficeDocumentViewer.csproj | 21 ---- .../OfficeDocumentViewer.razor | 5 - .../OfficeDocumentViewer.razor.cs | 103 ------------------ .../OfficeDocumentViewer.razor.js | 37 ------- .../_Imports.razor | 2 - .../wwwroot/office-viewer.css | 9 -- 6 files changed, 177 deletions(-) delete mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/BootstrapBlazor.OfficeDocumentViewer.csproj delete mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor delete mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.cs delete mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js delete mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/_Imports.razor delete mode 100644 src/components/BootstrapBlazor.OfficeDocumentViewer/wwwroot/office-viewer.css diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/BootstrapBlazor.OfficeDocumentViewer.csproj b/src/components/BootstrapBlazor.OfficeDocumentViewer/BootstrapBlazor.OfficeDocumentViewer.csproj deleted file mode 100644 index 0f96d940..00000000 --- a/src/components/BootstrapBlazor.OfficeDocumentViewer/BootstrapBlazor.OfficeDocumentViewer.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 9.0.0 - - - - Bootstrap Blazor WebAssembly wasm UI Components Office Viewer - Bootstrap UI components extensions of Microsoft Office Documentation Viewer - - - - - - - - - - - - diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor b/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor deleted file mode 100644 index b12ca85d..00000000 --- a/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor +++ /dev/null @@ -1,5 +0,0 @@ -@namespace BootstrapBlazor.Components -@inherits BootstrapModuleComponentBase -@attribute [JSModuleAutoLoader("./_content/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js", JSObjectReference = true, AutoInvokeDispose = false)] - -
diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.cs b/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.cs deleted file mode 100644 index 14cd3192..00000000 --- a/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Website: https://www.blazor.zone or https://argozhang.github.io/ - -using Microsoft.AspNetCore.Components; - -namespace BootstrapBlazor.Components; - -public partial class OfficeDocumentViewer -{ - /// - /// Gets or sets the url for the PDF file to be displayed. - /// - [Parameter] - public string? Url { get; set; } - - /// - /// Gets or sets the viewer height. Default is null. - /// - [Parameter] - public string? Height { get; set; } - - /// - /// Gets or sets the document loaded event callback. - /// - [Parameter] - public Func? OnLoaded { get; set; } - - [Inject, NotNull] - private NavigationManager? NavigationManager { get; set; } - - private string? ClassString => CssBuilder.Default("bb-office-viewer-container") - .AddClassFromAttributes(AdditionalAttributes) - .Build(); - - private string? StyleString => CssBuilder.Default() - .AddClass($"--bb-office-viewer-height: {Height};", !string.IsNullOrEmpty(Height)) - .Build(); - - private string? _url; - - /// - /// - /// - /// - /// - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); - - if (firstRender) - { - _url = Url; - return; - } - - var rerender = false; - if (_url != Url) - { - _url = Url; - rerender = true; - } - - if (rerender) - { - await InvokeVoidAsync("load", Id, GetAbsoluteUri(_url)); - } - } - - /// - /// - /// - /// - protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new - { - LoadedCallaback = nameof(TriggerOnLoaded), - Url = GetAbsoluteUri(Url) - }); - - private string GetAbsoluteUri(string? url) - { - url ??= string.Empty; - if (string.IsNullOrEmpty(url)) - { - return url; - } - var uri = NavigationManager.ToAbsoluteUri(url); - return uri.AbsoluteUri; - } - - /// - /// Trigger OnLoaded callback when the PDF document is loaded. - /// - /// - [JSInvokable] - public async Task TriggerOnLoaded() - { - if (OnLoaded != null) - { - await OnLoaded(); - } - } -} diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js b/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js deleted file mode 100644 index b9f8b87f..00000000 --- a/src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js +++ /dev/null @@ -1,37 +0,0 @@ -import { addLink } from "../BootstrapBlazor/modules/utility.js" -import Data from "../BootstrapBlazor/modules/data.js" - -export async function init(id, invoke, options) { - await addLink("./_content/BootstrapBlazor.OfficeDocumentViewer/office-viewer.css"); - - const el = document.getElementById(id); - const officeViewer = { el, invoke, options }; - Data.set(id, officeViewer); - - await load(id, options.url); -} - -export async function load(id, url) { - const officeViewer = Data.get(id); - const { el, invoke, options } = officeViewer; - - el.innerHTML = ''; - - if (url) { - const { frame } = officeViewer; - const viewer = frame || createFrame(el); - if (options.loadedCallaback) { - viewer.onload = () => { - invoke.invokeMethodAsync(options.loadedCallaback); - }; - } - viewer.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`; - } -} - -const createFrame = el => { - const frame = document.createElement('iframe'); - frame.classList.add('bb-office-viewer'); - el.appendChild(frame); - return frame; -} diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/_Imports.razor b/src/components/BootstrapBlazor.OfficeDocumentViewer/_Imports.razor deleted file mode 100644 index d46585f8..00000000 --- a/src/components/BootstrapBlazor.OfficeDocumentViewer/_Imports.razor +++ /dev/null @@ -1,2 +0,0 @@ -@using BootstrapBlazor.Components; -@using Microsoft.AspNetCore.Components.Web diff --git a/src/components/BootstrapBlazor.OfficeDocumentViewer/wwwroot/office-viewer.css b/src/components/BootstrapBlazor.OfficeDocumentViewer/wwwroot/office-viewer.css deleted file mode 100644 index 0cdbffd0..00000000 --- a/src/components/BootstrapBlazor.OfficeDocumentViewer/wwwroot/office-viewer.css +++ /dev/null @@ -1,9 +0,0 @@ -.bb-office-viewer-container { - width: 100%; - height: var(--bb-office-viewer-height, 500px); -} - -.bb-office-viewer { - width: 100%; - height: 100%; -}