diff --git a/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor b/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor index a528bf0b34d..3617e53e5a1 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor @@ -6,13 +6,15 @@

1. 服务注入

-
[Inject]
-[NotNull]
-private ITcpSocketFactory? TcpSocketFactory { get; set; }
+
services.AddBootstrapBlazorTcpSocketFactory();

2. 使用服务

调用 TcpSocketFactory 实例方法 GetOrCreate 即可得到一个 ITcpSocketClient 实例。内部提供复用机制,调用两次得到的 ITcpSocketClient 为同一对象

+
[Inject]
+[NotNull]
+private ITcpSocketFactory? TcpSocketFactory { get; set; }
+
var client = factory.GetOrCreate("192.168.1.100", 0);

3. 使用方法

diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Notice.razor b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Notice.razor new file mode 100644 index 00000000000..b618855b426 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Notice.razor @@ -0,0 +1,3 @@ + +

ITcpSocketFactory 服务仅在 Server 模式下可用

+
diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor new file mode 100644 index 00000000000..f62065ad5cd --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor @@ -0,0 +1,44 @@ +@page "/socket/receive" +@inject IStringLocalizer Localizer + +

@Localizer["ReceivesTitle"]

+

@Localizer["ReceivesDescription"]

+ + + + +

本例中连接一个模拟时间同步服务,每间隔 10s 自动发送服务器时间戳,连接后无需发送任何数据即可持续收到时间戳数据

+ +

使用 ReceivedCallBack 委托获得接收到的数据,可通过 += 方法支持多个客户端接收数据

+
_client.ReceivedCallBack += OnReceivedAsync;
+

特别注意页面需要继承 IDisposable 或者 IAsyncDisposable 接口,在 Dispose 或者 DisposeAsync 中移除委托

+
private void Dispose(bool disposing)
+{
+    if (disposing)
+    {
+        if (_client is { IsConnected: true })
+        {
+            _client.ReceivedCallBack -= OnReceivedAsync;
+        }
+    }
+}
+ +
+
+ + +
+
+ +
+
+
+ diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs new file mode 100644 index 00000000000..e20876a0077 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using System.Net; + +namespace BootstrapBlazor.Server.Components.Samples.Sockets; + +/// +/// +/// +public partial class Receives : ComponentBase, IDisposable +{ + [Inject, NotNull] + private ITcpSocketFactory? TcpSocketFactory { get; set; } + + private ITcpSocketClient _client = null!; + + private List _items = []; + + /// + /// + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + // 从服务中获取 Socket 实例 + _client = TcpSocketFactory.GetOrCreate("bb", key => new IPEndPoint(IPAddress.Loopback, 0)); + _client.ReceivedCallBack += OnReceivedAsync; + } + + private async Task OnConnectAsync() + { + if (_client is { IsConnected: false }) + { + await _client.ConnectAsync("127.0.0.1", 8800, CancellationToken.None); + } + } + + private async Task OnCloseAsync() + { + if (_client is { IsConnected: true }) + { + await _client.CloseAsync(); + } + } + + private Task OnClear() + { + _items = []; + return Task.CompletedTask; + } + + private async ValueTask OnReceivedAsync(ReadOnlyMemory data) + { + // 将数据显示为十六进制字符串 + var payload = System.Text.Encoding.UTF8.GetString(data.Span); + _items.Add(new ConsoleMessageItem + { + Message = $"接收到来自站点的数据为 {payload}" + }); + + // 保持队列中最大数量为 50 + if (_items.Count > 50) + { + _items.RemoveAt(0); + } + await InvokeAsync(StateHasChanged); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + if (_client is { IsConnected: true }) + { + _client.ReceivedCallBack -= OnReceivedAsync; + } + } + } + + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs index a90ea8df21e..3f0dcc34693 100644 --- a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs +++ b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs @@ -91,6 +91,13 @@ public static List GenerateMenus(this IStringLocalizer Locali }; AddSpeech(item); + item = new DemoMenuItem() + { + Text = Localizer["SocketComponents"], + Icon = "fa-solid fa-square-binary text-danger" + }; + AddSocket(item); + item = new DemoMenuItem() { Text = Localizer["Services"], @@ -205,6 +212,19 @@ void AddSpeech(DemoMenuItem item) AddBadge(item, count: 5); } + void AddSocket(DemoMenuItem item) + { + item.Items = new List + { + new() + { + Text = Localizer["SocketReceive"], + Url = "socket/receive" + } + }; + AddBadge(item, count: 1); + } + void AddQuickStar(DemoMenuItem item) { item.Items = new List diff --git a/src/BootstrapBlazor.Server/Extensions/ServiceCollectionExtensions.cs b/src/BootstrapBlazor.Server/Extensions/ServiceCollectionExtensions.cs index 6e0d00b0e1b..0692a824173 100644 --- a/src/BootstrapBlazor.Server/Extensions/ServiceCollectionExtensions.cs +++ b/src/BootstrapBlazor.Server/Extensions/ServiceCollectionExtensions.cs @@ -45,6 +45,7 @@ void Invoke(BootstrapBlazorOptions option) services.AddTaskServices(); services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); // 增加通用服务 services.AddBootstrapBlazorServices(); diff --git a/src/BootstrapBlazor.Server/Extensions/ServiceCollectionSharedExtensions.cs b/src/BootstrapBlazor.Server/Extensions/ServiceCollectionSharedExtensions.cs index 5022990d36d..13c4ae2790a 100644 --- a/src/BootstrapBlazor.Server/Extensions/ServiceCollectionSharedExtensions.cs +++ b/src/BootstrapBlazor.Server/Extensions/ServiceCollectionSharedExtensions.cs @@ -91,6 +91,9 @@ public static IServiceCollection AddBootstrapBlazorServices(this IServiceCollect // 增加 JuHe 定位服务 services.AddBootstrapBlazorJuHeIpLocatorService(); + // 增加 ITcpSocketFactory 服务 + services.AddBootstrapBlazorTcpSocketFactory(); + // 增加 PetaPoco ORM 数据服务操作类 // 需要时打开下面代码 //services.AddPetaPoco(option => diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 2c81246c0a8..068e756dd1b 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -4953,7 +4953,9 @@ "DropUpload": "DropUpload", "Vditor": "Vditor Markdown", "TcpSocketFactory": "ITcpSocketFactory", - "OfficeViewer": "Office Viewer" + "OfficeViewer": "Office Viewer", + "SocketComponents": "ITcpSocketFactory", + "SocketReceive": "Receive" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": { "TablesHeaderTitle": "Header grouping function", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index c36c64523c0..2f39134c5d0 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -4953,7 +4953,9 @@ "DropUpload": "拖动上传组件 DropUpload", "Vditor": "富文本框 Vditor Markdown", "TcpSocketFactory": "套接字服务 ITcpSocketFactory", - "OfficeViewer": "Office 文档预览组件" + "OfficeViewer": "Office 文档预览组件", + "SocketComponents": "Socket 服务", + "SocketReceive": "接收数据" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": { "TablesHeaderTitle": "表头分组功能", diff --git a/src/BootstrapBlazor.Server/Services/MockSocketServerService.cs b/src/BootstrapBlazor.Server/Services/MockSocketServerService.cs new file mode 100644 index 00000000000..113707a6705 --- /dev/null +++ b/src/BootstrapBlazor.Server/Services/MockSocketServerService.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using System.Net; +using System.Net.Sockets; + +namespace Longbow.Tasks.Services; + +/// +/// 模拟 Socket 服务端服务类 +/// +internal class MockSocketServerService(ILogger logger) : BackgroundService +{ + /// + /// 运行任务 + /// + /// + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var server = new TcpListener(IPAddress.Loopback, 8800); + server.Start(); + while (stoppingToken is { IsCancellationRequested: false }) + { + try + { + var client = await server.AcceptTcpClientAsync(stoppingToken); + _ = Task.Run(() => MockSendAsync(client, stoppingToken), stoppingToken); + } + catch { } + } + } + + private async Task MockSendAsync(TcpClient client, CancellationToken stoppingToken) + { + // 方法目的: + // 1. 模拟服务器间隔 10秒 发送当前时间戳数据包到客户端 + await using var stream = client.GetStream(); + while (stoppingToken is { IsCancellationRequested: false }) + { + try + { + var data = System.Text.Encoding.UTF8.GetBytes(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + await stream.WriteAsync(data, stoppingToken); + + await Task.Delay(10 * 1000, stoppingToken); + } + catch (OperationCanceledException) { break; } + catch (IOException) { break; } + catch (SocketException) { break; } + catch (Exception ex) + { + logger.LogError(ex, "MockSocketServerService encountered an error while sending data."); + break; + } + } + } +} diff --git a/src/BootstrapBlazor.Server/docs.json b/src/BootstrapBlazor.Server/docs.json index 71a4b1105d6..c56a1f057d9 100644 --- a/src/BootstrapBlazor.Server/docs.json +++ b/src/BootstrapBlazor.Server/docs.json @@ -242,7 +242,8 @@ "meet": "Meets", "vditor": "Vditors", "socket-factory": "SocketFactories", - "office-viewer": "OfficeViewers" + "office-viewer": "OfficeViewers", + "socket/receive": "Sockets\\Receives" }, "video": { "table": "BV1ap4y1x7Qn?p=1", diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 6efbf009919..498b9a5fc30 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@  - 9.8.0-beta06 + 9.8.0-beta07 diff --git a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs index 8b2839a8392..45837b5e12d 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/DefaultTcpSocketClient.cs @@ -21,7 +21,7 @@ sealed class DefaultTcpSocketClient(IPEndPoint localEndPoint) : TcpSocketClientB public override bool IsConnected => _client?.Connected ?? false; [NotNull] - public ILogger? Logger { get; set; } + public ILogger? Logger { get; init; } public override async ValueTask ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) { @@ -33,6 +33,8 @@ public override async ValueTask ConnectAsync(IPEndPoint endPoint, Cancella // 创建新的 TcpClient 实例 _client ??= new TcpClient(localEndPoint); + LocalEndPoint = localEndPoint; + _remoteEndPoint = null; var connectionToken = token; if (ConnectTimeout > 0) @@ -43,26 +45,27 @@ public override async ValueTask ConnectAsync(IPEndPoint endPoint, Cancella } await _client.ConnectAsync(endPoint, connectionToken); - // 设置本地以及远端端点信息 - LocalEndPoint = (IPEndPoint)_client.Client.LocalEndPoint!; - _remoteEndPoint = endPoint; - - if (IsAutoReceive) + if (_client.Connected) { - _ = Task.Run(AutoReceiveAsync); + _remoteEndPoint = endPoint; + + // 设置本地端点信息 + if (_client.Client.LocalEndPoint is IPEndPoint local) + { + LocalEndPoint = local; + } + if (IsAutoReceive) + { + _ = Task.Run(AutoReceiveAsync, token); + } } - ret = true; + ret = _client.Connected; } 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); - } + Logger.LogWarning(ex, token.IsCancellationRequested + ? "TCP Socket connect operation was canceled from {LocalEndPoint} to {RemoteEndPoint}" + : "TCP Socket connect operation timed out from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, endPoint); } catch (Exception ex) { @@ -101,14 +104,9 @@ public override async ValueTask SendAsync(ReadOnlyMemory data, Cance } 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); - } + Logger.LogWarning(ex, token.IsCancellationRequested + ? "TCP Socket send operation was canceled from {LocalEndPoint} to {RemoteEndPoint}" + : "TCP Socket send operation timed out from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); } catch (Exception ex) { @@ -119,7 +117,7 @@ public override async ValueTask SendAsync(ReadOnlyMemory data, Cance public override async ValueTask> ReceiveAsync(CancellationToken token = default) { - if (_client == null || !_client.Connected) + if (_client is not { Connected: true }) { throw new InvalidOperationException($"TCP Socket is not connected {LocalEndPoint}"); } @@ -132,7 +130,7 @@ public override async ValueTask> ReceiveAsync(CancellationToken tok using var block = MemoryPool.Shared.Rent(ReceiveBufferSize); var buffer = block.Memory; var len = await ReceiveCoreAsync(_client, buffer, token); - return buffer[0..len]; + return buffer[..len]; } private async ValueTask AutoReceiveAsync() @@ -188,18 +186,14 @@ private async ValueTask ReceiveCoreAsync(TcpClient client, Memory buf { await DataPackageHandler.ReceiveAsync(buffer, receiveToken); } + len = buffer.Length; } } 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); - } + Logger.LogWarning(ex, token.IsCancellationRequested + ? "TCP Socket receive operation canceled from {LocalEndPoint} to {RemoteEndPoint}" + : "TCP Socket receive operation timed out from {LocalEndPoint} to {RemoteEndPoint}", LocalEndPoint, _remoteEndPoint); } catch (Exception ex) { @@ -214,9 +208,6 @@ protected override async ValueTask DisposeAsync(bool disposing) if (disposing) { - LocalEndPoint = null; - _remoteEndPoint = null; - // 取消接收数据的任务 if (_receiveCancellationTokenSource != null) { diff --git a/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs b/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs index 81fde060e97..c88529509fa 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/Extensions/TcpSocketExtensions.cs @@ -16,7 +16,7 @@ namespace BootstrapBlazor.Components; public static class TcpSocketExtensions { /// - /// 增加 + /// 增加 ITcpSocketFactory 服务 /// /// /// diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs index a9f2e5349de..3eba6680e6b 100644 --- a/test/UnitTest/Services/TcpSocketFactoryTest.cs +++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs @@ -497,7 +497,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) {