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);
+});
+
+ - 点击 连接 按钮后通过
ITcpSocketFactory 服务实例创建的 ITcpSocketClient 对象连接到网站模拟 TcpServer
+ - 点击 断开 按钮调用
CloseAsync 方法断开 Socket 连接
+ - 点击 发送 按钮调用
SendAsync 方法发送请求数据
+
+ 通讯协议讲解:
+ 在实际应用开发中,通讯数据协议很多时候是双方约定的。我们假设本示例通讯协议规约为定长格式具体如下:
+
+ - 发送数据包格式为
请求头(Header)+ 请求体(Body) 长度总和为 12 个字节
+ - 请求头为 4 字节定长,请求体为 8 个字节定长
+ - 请求体为字符串类型数据
+ - 返回数据包格式为
响应头(Header)+ 响应体(Body) 长度总和为 12 个字节
+ - 响应头为 4 字节定长,响应体为 8 个字节定长
+ - 响应体为字符串类型数据
+
+
+
+
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);