diff --git a/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor b/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor index 3617e53e5a1..7e220d4476b 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/SocketFactories.razor @@ -15,7 +15,10 @@ [NotNull] private ITcpSocketFactory? TcpSocketFactory { get; set; } -
var client = factory.GetOrCreate("192.168.1.100", 0);
+
var client = TcpSocketFactory.GetOrCreate("bb", options =>
+{
+    options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0);
+});

3. 使用方法

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

@Localizer["AdaptersTitle"]

+

@Localizer["AdaptersDescription"]

+ + + + +

本例中连接一个模拟自定义协议服务,每次接收到客户端发来的特定数据后,返回业务数据。这类应用在我们实际应用中非常常见

+

通过 SocketClientOptions 配置类关闭自动接收数据功能 IsAutoReceive="false"

+
_client = TcpSocketFactory.GetOrCreate("demo-adapter", options =>
+{
+    options.IsAutoReceive = false;
+    options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0);
+});
+ +

通讯协议讲解:

+

在实际应用开发中,通讯数据协议很多时候是双方约定的。我们假设本示例通讯协议规约为定长格式具体如下:

+ + +
+
+ + + +
+
+ +
+
+
diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs new file mode 100644 index 00000000000..488174594d2 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs @@ -0,0 +1,136 @@ +// 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 BootstrapBlazor.Server.Components.Components; +using System.Net; +using System.Text; + +namespace BootstrapBlazor.Server.Components.Samples.Sockets; + +/// +/// 数据适配器示例 +/// +public partial class Adapters : IDisposable +{ + [Inject, NotNull] + private ITcpSocketFactory? TcpSocketFactory { get; set; } + + private ITcpSocketClient _client = null!; + + private List _items = []; + + private readonly IPEndPoint _serverEndPoint = new(IPAddress.Loopback, 8900); + + private readonly CancellationTokenSource _connectTokenSource = new(); + private readonly CancellationTokenSource _sendTokenSource = new(); + private readonly CancellationTokenSource _receiveTokenSource = new(); + + /// + /// + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + // 从服务中获取 ITcpSocketClient 实例 + _client = TcpSocketFactory.GetOrCreate("demo-adapter", options => + { + // 关闭自动接收功能 + options.IsAutoReceive = false; + // 设置本地使用的 IP地址与端口 + options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + }); + } + + private async Task OnConnectAsync() + { + if (_client is { IsConnected: false }) + { + await _client.ConnectAsync(_serverEndPoint, _connectTokenSource.Token); + var state = _client.IsConnected ? "成功" : "失败"; + _items.Add(new ConsoleMessageItem() + { + Message = $"{DateTime.Now}: 连接 {_client.LocalEndPoint} - {_serverEndPoint} {state}", + Color = _client.IsConnected ? Color.Success : Color.Danger + }); + } + } + + private async Task OnSendAsync() + { + if (_client is { IsConnected: true }) + { + // 准备通讯数据 + var data = new byte[12]; + "2025"u8.CopyTo(data); + Encoding.UTF8.GetBytes(DateTime.Now.ToString("ddHHmmss")).CopyTo(data, 4); + var result = await _client.SendAsync(data, _sendTokenSource.Token); + if (result) + { + // 发送成功 + var payload = await _client.ReceiveAsync(_receiveTokenSource.Token); + if (!payload.IsEmpty) + { + // 解析接收到的数据 + // 响应头: 4 字节表示响应体长度 [0x32, 0x30, 0x32, 0x35] + // 响应体: 8 字节当前时间戳字符串 + data = payload.ToArray(); + var body = BitConverter.ToString(data); + _items.Add(new ConsoleMessageItem() + { + Message = $"{DateTime.Now}: 接收到来自 {_serverEndPoint} 数据: {Encoding.UTF8.GetString(data)} HEX: {body}" + }); + } + } + } + } + + private async Task OnCloseAsync() + { + if (_client is { IsConnected: true }) + { + await _client.CloseAsync(); + var state = _client.IsConnected ? "失败" : "成功"; + _items.Add(new ConsoleMessageItem() + { + Message = $"{DateTime.Now}: 关闭 {_client.LocalEndPoint} - {_serverEndPoint} {state}", + Color = _client.IsConnected ? Color.Danger : Color.Success + }); + } + } + + private Task OnClear() + { + _items = []; + return Task.CompletedTask; + } + + private void Dispose(bool disposing) + { + if (disposing) + { + // 释放连接令牌资源 + _connectTokenSource.Cancel(); + _connectTokenSource.Dispose(); + + // 释放发送令牌资源 + _sendTokenSource.Cancel(); + _sendTokenSource.Dispose(); + + // 释放接收令牌资源 + _receiveTokenSource.Cancel(); + _receiveTokenSource.Dispose(); + } + } + + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs index e20876a0077..c8977842423 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs @@ -8,9 +8,9 @@ namespace BootstrapBlazor.Server.Components.Samples.Sockets; /// -/// +/// 接收电文示例 /// -public partial class Receives : ComponentBase, IDisposable +public partial class Receives : IDisposable { [Inject, NotNull] private ITcpSocketFactory? TcpSocketFactory { get; set; } @@ -19,6 +19,8 @@ public partial class Receives : ComponentBase, IDisposable private List _items = []; + private readonly IPEndPoint _serverEndPoint = new(IPAddress.Loopback, 8800); + /// /// /// @@ -27,7 +29,10 @@ protected override void OnInitialized() base.OnInitialized(); // 从服务中获取 Socket 实例 - _client = TcpSocketFactory.GetOrCreate("bb", key => new IPEndPoint(IPAddress.Loopback, 0)); + _client = TcpSocketFactory.GetOrCreate("demo-receive", options => + { + options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + }); _client.ReceivedCallBack += OnReceivedAsync; } @@ -35,7 +40,7 @@ private async Task OnConnectAsync() { if (_client is { IsConnected: false }) { - await _client.ConnectAsync("127.0.0.1", 8800, CancellationToken.None); + await _client.ConnectAsync(_serverEndPoint, CancellationToken.None); } } diff --git a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs index 3f0dcc34693..662b9cf8a61 100644 --- a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs +++ b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs @@ -218,8 +218,15 @@ void AddSocket(DemoMenuItem item) { new() { + IsNew = true, Text = Localizer["SocketReceive"], Url = "socket/receive" + }, + new() + { + IsNew = true, + Text = Localizer["SocketDataAdapter"], + Url = "socket/adapter" } }; AddBadge(item, count: 1); @@ -257,7 +264,6 @@ void AddQuickStar(DemoMenuItem item) }, new() { - IsUpdate = true, Text = Localizer["Labels"], Url = "label" }, @@ -430,7 +436,6 @@ void AddForm(DemoMenuItem item) }, new() { - IsNew = true, Text = Localizer["OtpInput"], Url = "otp-input" }, @@ -467,13 +472,11 @@ void AddForm(DemoMenuItem item) }, new() { - IsUpdate = true, Text = Localizer["SelectTable"], Url = "select-table" }, new() { - IsUpdate = true, Text = Localizer["SelectTree"], Url = "select-tree" }, @@ -539,7 +542,6 @@ void AddForm(DemoMenuItem item) }, new() { - IsNew = true, Text = Localizer["Vditor"], Url = "vditor" } @@ -830,13 +832,11 @@ void AddData(DemoMenuItem item) }, new() { - IsNew = true, Text = Localizer["Typed"], Url = "typed" }, new() { - IsNew = true, Text = Localizer["UniverSheet"], Url = "univer-sheet" }, @@ -1241,7 +1241,6 @@ void AddNotice(DemoMenuItem item) }, new() { - IsNew = true, Text = Localizer["FullScreenButton"], Url = "fullscreen-button" }, @@ -1267,7 +1266,6 @@ void AddNotice(DemoMenuItem item) }, new() { - IsNew = true, Text = Localizer["Meet"], Url = "meet" }, @@ -1568,7 +1566,6 @@ void AddServices(DemoMenuItem item) }, new() { - IsNew = true, Text = Localizer["Html2Image"], Url = "html2image" }, @@ -1615,7 +1612,6 @@ void AddServices(DemoMenuItem item) }, new() { - IsNew = true, Text = Localizer["TotpService"], Url = "otp-service" }, @@ -1626,13 +1622,11 @@ void AddServices(DemoMenuItem item) }, new() { - IsNew = true, Text = Localizer["AudioDevice"], Url = "audio-device" }, new() { - IsNew = true, Text = Localizer["VideoDevice"], Url = "video-device" }, diff --git a/src/BootstrapBlazor.Server/Extensions/ServiceCollectionExtensions.cs b/src/BootstrapBlazor.Server/Extensions/ServiceCollectionExtensions.cs index 0692a824173..2687f60d537 100644 --- a/src/BootstrapBlazor.Server/Extensions/ServiceCollectionExtensions.cs +++ b/src/BootstrapBlazor.Server/Extensions/ServiceCollectionExtensions.cs @@ -45,7 +45,8 @@ void Invoke(BootstrapBlazorOptions option) services.AddTaskServices(); services.AddHostedService(); services.AddHostedService(); - services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); // 增加通用服务 services.AddBootstrapBlazorServices(); diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 068e756dd1b..cbfa88e0e44 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -7210,5 +7210,11 @@ "OfficeViewerNormalTitle": "Basic Usage", "OfficeViewerNormalIntro": "Set the document URL for preview by configuring the Url value", "OfficeViewerToastSuccessfulContent": "Office document loaded successfully" + }, + "BootstrapBlazor.Server.Components.Samples.Sockets.Receives": { + "ReceivesTitle": "Socket Receive", + "ReceivesDescription": "Receive data through Socket and display it", + "NormalTitle": "Basic usage", + "NormalIntro": "After connecting, the timestamp data sent by the server is automatically received through the ReceivedCallBack callback method" } } diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 2f39134c5d0..b012a2ed8c5 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -7210,5 +7210,11 @@ "OfficeViewerNormalTitle": "基本用法", "OfficeViewerNormalIntro": "通过设置 Url 值设置预览文档地址", "OfficeViewerToastSuccessfulContent": "Office 文档加载成功" + }, + "BootstrapBlazor.Server.Components.Samples.Sockets.Receives": { + "ReceivesTitle": "Socket 接收示例", + "ReceivesDescription": "通过 Socket 接收数据并且显示", + "NormalTitle": "基本用法", + "NormalIntro": "连接后通过 ReceivedCallBack 回调方法自动接收服务端发送来的时间戳数据" } } diff --git a/src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs b/src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs new file mode 100644 index 00000000000..d9ed2acabd9 --- /dev/null +++ b/src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs @@ -0,0 +1,74 @@ +// 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 MockCustomProtocolSocketServerService(ILogger logger) : BackgroundService +{ + /// + /// 运行任务 + /// + /// + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var server = new TcpListener(IPAddress.Loopback, 8900); + server.Start(); + while (stoppingToken is { IsCancellationRequested: false }) + { + try + { + var client = await server.AcceptTcpClientAsync(stoppingToken); + _ = Task.Run(() => OnDataHandlerAsync(client, stoppingToken), stoppingToken); + } + catch { } + } + } + + private async Task OnDataHandlerAsync(TcpClient client, CancellationToken stoppingToken) + { + // 方法目的: + // 收到消息后发送自定义通讯协议的响应数据 + // 响应头 + 响应体 + await using var stream = client.GetStream(); + while (stoppingToken is { IsCancellationRequested: false }) + { + try + { + // 接收数据 + var len = await stream.ReadAsync(new byte[1024], stoppingToken); + if (len == 0) + { + // 断开连接 + break; + } + + // 实际应用中需要解析接收到的数据进行处理,本示例中仅模拟接收数据后发送响应数据 + + // 发送响应数据 + // 响应头: 4 字节表示响应体长度 [0x32, 0x30, 0x32, 0x35] + // 响应体: 8 字节当前时间戳字符串 + var data = new byte[12]; + "2025"u8.ToArray().CopyTo(data, 0); + System.Text.Encoding.UTF8.GetBytes(DateTime.Now.ToString("ddHHmmss")).CopyTo(data, 4); + await stream.WriteAsync(data, stoppingToken); + } + catch (OperationCanceledException) { break; } + catch (IOException) { break; } + catch (SocketException) { break; } + catch (Exception ex) + { + logger.LogError(ex, "MockCustomProtocolSocketServerService encountered an error while sending data."); + break; + } + } + } +} diff --git a/src/BootstrapBlazor.Server/Services/MockSocketServerService.cs b/src/BootstrapBlazor.Server/Services/MockReceiveSocketServerService.cs similarity index 89% rename from src/BootstrapBlazor.Server/Services/MockSocketServerService.cs rename to src/BootstrapBlazor.Server/Services/MockReceiveSocketServerService.cs index 113707a6705..f66e08e5128 100644 --- a/src/BootstrapBlazor.Server/Services/MockSocketServerService.cs +++ b/src/BootstrapBlazor.Server/Services/MockReceiveSocketServerService.cs @@ -11,7 +11,7 @@ namespace Longbow.Tasks.Services; /// /// 模拟 Socket 服务端服务类 /// -internal class MockSocketServerService(ILogger logger) : BackgroundService +class MockReceiveSocketServerService(ILogger logger) : BackgroundService { /// /// 运行任务 @@ -52,7 +52,7 @@ private async Task MockSendAsync(TcpClient client, CancellationToken stoppingTok catch (SocketException) { break; } catch (Exception ex) { - logger.LogError(ex, "MockSocketServerService encountered an error while sending data."); + logger.LogError(ex, "MockReceiveSocketServerService encountered an error while sending data."); break; } } diff --git a/src/BootstrapBlazor.Server/docs.json b/src/BootstrapBlazor.Server/docs.json index c56a1f057d9..20fe9370e93 100644 --- a/src/BootstrapBlazor.Server/docs.json +++ b/src/BootstrapBlazor.Server/docs.json @@ -243,7 +243,8 @@ "vditor": "Vditors", "socket-factory": "SocketFactories", "office-viewer": "OfficeViewers", - "socket/receive": "Sockets\\Receives" + "socket/receive": "Sockets\\Receives", + "socket/adapter": "Sockets\\Adapters" }, "video": { "table": "BV1ap4y1x7Qn?p=1", diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs index 05aff55447b..5c6c0960246 100644 --- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs +++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs @@ -16,7 +16,7 @@ namespace BootstrapBlazor.Components; /// clients. /// /// The class offers core functionality for managing TCP socket -/// connections, including connecting to remote endpoints, sending and receiving data, and handling data packages. +/// connections, including connecting to remote endpoints, sending and receiving data, and handling data packages. /// Derived classes can extend or override its behavior to implement specific client logic. Key features include: - /// Connection management with support for timeouts and cancellation tokens. - Data transmission and reception with /// optional data package handling. - Logging capabilities for tracking events and errors. - Dependency injection @@ -30,7 +30,6 @@ public abstract class TcpSocketClientBase(SocketClientOptions options) : ITcpSoc /// protected ISocketClientProvider? SocketClientProvider { get; set; } - /// /// Gets or sets the logger instance used for logging messages and events. /// @@ -273,7 +272,7 @@ private async ValueTask ReceiveCoreAsync(ISocketClientProvider client, Memo /// /// Logs a message with the specified log level, exception, and additional context. /// - protected void Log(LogLevel logLevel, Exception? ex, string? message) + protected virtual void Log(LogLevel logLevel, Exception? ex, string? message) { Logger ??= ServiceProvider?.GetRequiredService>(); Logger?.Log(logLevel, ex, "{Message}", message);