From df88564fdd1d300e082890fc9b0b2c584e408983 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 29 Jun 2025 16:22:47 +0800 Subject: [PATCH 1/8] =?UTF-8?q?doc:=20=E5=A2=9E=E5=8A=A0=E6=8E=A5=E6=94=B6?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=A4=BA=E4=BE=8B=E6=96=87=E6=A1=A3=E8=AF=B4?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/Sockets/Receives.razor.cs | 7 +++++-- src/BootstrapBlazor.Server/Locales/en-US.json | 6 ++++++ src/BootstrapBlazor.Server/Locales/zh-CN.json | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs index e20876a0077..2e7d26a6b22 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs @@ -8,7 +8,7 @@ namespace BootstrapBlazor.Server.Components.Samples.Sockets; /// -/// +/// 接收电文示例 /// public partial class Receives : ComponentBase, IDisposable { @@ -27,7 +27,10 @@ protected override void OnInitialized() base.OnInitialized(); // 从服务中获取 Socket 实例 - _client = TcpSocketFactory.GetOrCreate("bb", key => new IPEndPoint(IPAddress.Loopback, 0)); + _client = TcpSocketFactory.GetOrCreate("bb", options => + { + options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + }); _client.ReceivedCallBack += OnReceivedAsync; } 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 回调方法自动接收服务端发送来的时间戳数据" } } From b56174bd5e6df4c59783827eaf8590f600a6556a Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 29 Jun 2025 16:54:50 +0800 Subject: [PATCH 2/8] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/TcpSocket/TcpSocketClientBase.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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); From ad932d8bc80977b66556da77f038475176932e6f Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 29 Jun 2025 16:55:05 +0800 Subject: [PATCH 3/8] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=B1=BB=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/SocketFactories.razor | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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. 使用方法

From 219008b245246fc0f742a5c851ef594bc931c651 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 29 Jun 2025 16:55:18 +0800 Subject: [PATCH 4/8] =?UTF-8?q?doc:=20=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=80=82=E9=85=8D=E5=99=A8=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/Sockets/Adapters.razor | 40 +++++++ .../Samples/Sockets/Adapters.razor.cs | 110 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor create mode 100644 src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs 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..c6907eae62a --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor @@ -0,0 +1,40 @@ +@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);
+});
+
    +
  • 点击 连接 按钮后通过 ITcpSocketFactory 服务实例创建的 ITcpSocketClient 对象连接到网站模拟 TcpServer
  • +
  • 点击 断开 按钮调用 CloseAsync 方法断开 Socket 连接
  • +
  • 点击 发送 按钮调用 SendAsync 方法发送请求数据
  • +
+

本例中已关闭自动接收功能,调用 ReceivedAsync 方法主动接收服务端返回数据

+ +
+
+ + + +
+
+ +
+
+
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..2cd267af947 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs @@ -0,0 +1,110 @@ +// 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 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 CancellationTokenSource _connectTokenSource = new(); + private CancellationTokenSource _sendTokenSource = new(); + + /// + /// + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + // 从服务中获取 Socket 实例 + _client = TcpSocketFactory.GetOrCreate("demo-adapter", options => + { + options.IsAutoReceive = false; + options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + }); + } + + private async Task OnConnectAsync() + { + if (_client is { IsConnected: false }) + { + await _client.ConnectAsync(_serverEndPoint, _connectTokenSource.Token); + } + } + + private async Task SendAsync() + { + if (_client is { IsConnected: false }) + { + // 准备通讯数据 + var data = new byte[1024]; + await _client.SendAsync(data, _sendTokenSource.Token); + } + } + + 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 }) + { + + } + } + } + + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} From 6cbad201d5b06ebb4e026408a49ac5b382e7cfb6 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 29 Jun 2025 16:55:31 +0800 Subject: [PATCH 5/8] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E6=8E=A5?= =?UTF-8?q?=E6=94=B6=E6=95=B0=E6=8D=AE=E7=A4=BA=E4=BE=8B=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/Sockets/Receives.razor.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs index 2e7d26a6b22..c8977842423 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Receives.razor.cs @@ -10,7 +10,7 @@ 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,7 @@ protected override void OnInitialized() base.OnInitialized(); // 从服务中获取 Socket 实例 - _client = TcpSocketFactory.GetOrCreate("bb", options => + _client = TcpSocketFactory.GetOrCreate("demo-receive", options => { options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); }); @@ -38,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); } } From b17c4c64dea90fe2ba662eb5d22417c459101731 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 29 Jun 2025 16:55:42 +0800 Subject: [PATCH 6/8] =?UTF-8?q?doc:=20=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=80=82=E9=85=8D=E5=99=A8=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/MenusLocalizerExtensions.cs | 20 +++++++------------ src/BootstrapBlazor.Server/docs.json | 3 ++- 2 files changed, 9 insertions(+), 14 deletions(-) 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/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", From 3c17b3298e6d9f57825e5d1978f32ccc7d728019 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 29 Jun 2025 18:08:06 +0800 Subject: [PATCH 7/8] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/Sockets/Adapters.razor | 13 ++- .../Samples/Sockets/Adapters.razor.cs | 80 ++++++++++++------- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor index c6907eae62a..b3d4ea5509e 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor @@ -21,7 +21,16 @@
  • 点击 断开 按钮调用 CloseAsync 方法断开 Socket 连接
  • 点击 发送 按钮调用 SendAsync 方法发送请求数据
  • -

    本例中已关闭自动接收功能,调用 ReceivedAsync 方法主动接收服务端返回数据

    +

    通讯协议讲解:

    +

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

    +
      +
    • 发送数据包格式为 请求头(Header)+ 请求体(Body) 长度总和为 12 个字节
    • +
    • 请求头为 4 字节定长,请求体为 8 个字节定长
    • +
    • 请求体为字符串类型数据
    • +
    • 返回数据包格式为 响应头(Header)+ 响应体(Body) 长度总和为 12 个字节
    • +
    • 响应头为 4 字节定长,响应体为 8 个字节定长
    • +
    • 响应体为字符串类型数据
    • +
    @@ -33,7 +42,7 @@ OnClick="OnSendAsync" IsDisabled="@(!_client.IsConnected)">
    -
    diff --git a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs index 2cd267af947..488174594d2 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs @@ -3,7 +3,9 @@ // 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; @@ -21,8 +23,9 @@ public partial class Adapters : IDisposable private readonly IPEndPoint _serverEndPoint = new(IPAddress.Loopback, 8900); - private CancellationTokenSource _connectTokenSource = new(); - private CancellationTokenSource _sendTokenSource = new(); + private readonly CancellationTokenSource _connectTokenSource = new(); + private readonly CancellationTokenSource _sendTokenSource = new(); + private readonly CancellationTokenSource _receiveTokenSource = new(); /// /// @@ -31,10 +34,12 @@ protected override void OnInitialized() { base.OnInitialized(); - // 从服务中获取 Socket 实例 + // 从服务中获取 ITcpSocketClient 实例 _client = TcpSocketFactory.GetOrCreate("demo-adapter", options => { + // 关闭自动接收功能 options.IsAutoReceive = false; + // 设置本地使用的 IP地址与端口 options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); }); } @@ -44,16 +49,41 @@ 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 SendAsync() + private async Task OnSendAsync() { - if (_client is { IsConnected: false }) + if (_client is { IsConnected: true }) { // 准备通讯数据 - var data = new byte[1024]; - await _client.SendAsync(data, _sendTokenSource.Token); + 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}" + }); + } + } } } @@ -62,6 +92,12 @@ 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 + }); } } @@ -71,31 +107,21 @@ private Task OnClear() 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 }) - { + // 释放连接令牌资源 + _connectTokenSource.Cancel(); + _connectTokenSource.Dispose(); - } + // 释放发送令牌资源 + _sendTokenSource.Cancel(); + _sendTokenSource.Dispose(); + + // 释放接收令牌资源 + _receiveTokenSource.Cancel(); + _receiveTokenSource.Dispose(); } } From f9c42cfaa11f7a85202e8e148bf7b4a52cdf0a6b Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 29 Jun 2025 18:08:17 +0800 Subject: [PATCH 8/8] =?UTF-8?q?doc:=20=E5=A2=9E=E5=8A=A0=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/ServiceCollectionExtensions.cs | 3 +- .../MockCustomProtocalSocketServerService.cs | 74 +++++++++++++++++++ ...e.cs => MockReceiveSocketServerService.cs} | 4 +- 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs rename src/BootstrapBlazor.Server/Services/{MockSocketServerService.cs => MockReceiveSocketServerService.cs} (89%) 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/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; } }