Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
[NotNull]
private ITcpSocketFactory? TcpSocketFactory { get; set; }</Pre>

<Pre>var client = factory.GetOrCreate("192.168.1.100", 0);</Pre>
<Pre>var client = TcpSocketFactory.GetOrCreate("bb", options =>
{
options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0);
});</Pre>

<p class="code-label">3. 使用方法</p>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@page "/socket/adapter"
@inject IStringLocalizer<Adapters> Localizer

<h3>@Localizer["AdaptersTitle"]</h3>
<h4>@Localizer["AdaptersDescription"]</h4>

<Notice></Notice>

<DemoBlock Title="@Localizer["NormalTitle"]"
Introduction="@Localizer["NormalIntro"]"
Name="Normal" ShowCode="false">
<p>本例中连接一个模拟自定义协议服务,每次接收到客户端发来的特定数据后,返回业务数据。这类应用在我们实际应用中非常常见</p>
<p>通过 <code>SocketClientOptions</code> 配置类关闭自动接收数据功能 <code>IsAutoReceive="false"</code></p>
<Pre>_client = TcpSocketFactory.GetOrCreate("demo-adapter", options =>
{
options.IsAutoReceive = false;
options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0);
});</Pre>
<ul class="ul-demo">
<li>点击 <b>连接</b> 按钮后通过 <code>ITcpSocketFactory</code> 服务实例创建的 <code>ITcpSocketClient</code> 对象连接到网站模拟 <code>TcpServer</code></li>
<li>点击 <b>断开</b> 按钮调用 <code>CloseAsync</code> 方法断开 Socket 连接</li>
<li>点击 <b>发送</b> 按钮调用 <code>SendAsync</code> 方法发送请求数据</li>
</ul>
<p class="code-label">通讯协议讲解:</p>
<p>在实际应用开发中,通讯数据协议很多时候是双方约定的。我们假设本示例通讯协议规约为定长格式具体如下:</p>
<ul class="ul-demo">
<li>发送数据包格式为 <code>请求头(Header)+ 请求体(Body)</code> 长度总和为 12 个字节</li>
<li>请求头为 4 字节定长,请求体为 8 个字节定长</li>
<li>请求体为字符串类型数据</li>
<li>返回数据包格式为 <code>响应头(Header)+ 响应体(Body)</code> 长度总和为 12 个字节</li>
<li>响应头为 4 字节定长,响应体为 8 个字节定长</li>
<li>响应体为字符串类型数据</li>
</ul>

<div class="row form-inline g-3">
<div class="col-12 col-sm-6">
<Button Text="连接" Icon="fa-solid fa-play"
OnClick="OnConnectAsync" IsDisabled="@_client.IsConnected"></Button>
<Button Text="断开" Icon="fa-solid fa-stop" class="ms-2"
OnClick="OnCloseAsync" IsDisabled="@(!_client.IsConnected)"></Button>
<Button Text="发送" Icon="fa-solid fa-paper-plane" class="ms-2" IsAsync="true"
OnClick="OnSendAsync" IsDisabled="@(!_client.IsConnected)"></Button>
</div>
<div class="col-12">
<Console Items="@_items" Height="496" HeaderText="模拟通讯示例"
ShowAutoScroll="true" OnClear="@OnClear"></Console>
</div>
</div>
</DemoBlock>
Original file line number Diff line number Diff line change
@@ -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([email protected]) Website: https://www.blazor.zone

using BootstrapBlazor.Server.Components.Components;
using System.Net;
using System.Text;

namespace BootstrapBlazor.Server.Components.Samples.Sockets;

/// <summary>
/// 数据适配器示例
/// </summary>
public partial class Adapters : IDisposable
{
[Inject, NotNull]
private ITcpSocketFactory? TcpSocketFactory { get; set; }

private ITcpSocketClient _client = null!;

private List<ConsoleMessageItem> _items = [];

private readonly IPEndPoint _serverEndPoint = new(IPAddress.Loopback, 8900);

private readonly CancellationTokenSource _connectTokenSource = new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider consolidating cancellation logic and extracting repeated code into helpers to simplify and clarify the component.

Suggested change
private readonly CancellationTokenSource _connectTokenSource = new();
// 1. Replace three CancellationTokenSources with a single one:
-private readonly CancellationTokenSource _connectTokenSource = new();
-private readonly CancellationTokenSource _sendTokenSource = new();
-private readonly CancellationTokenSource _receiveTokenSource = new();
+private readonly CancellationTokenSource _cts = new();
// …and in Dispose:
-public void Dispose()
-{
- _connectTokenSource.Cancel();
- _connectTokenSource.Dispose();
- _sendTokenSource.Cancel();
- _sendTokenSource.Dispose();
- _receiveTokenSource.Cancel();
- _receiveTokenSource.Dispose();
- GC.SuppressFinalize(this);
-}
+public void Dispose()
+{
+ _cts.Cancel();
+ _cts.Dispose();
+ GC.SuppressFinalize(this);
+}
// 2. Extract message‐logging helper to remove repetition:
-private async Task OnConnectAsync()
+private Task OnConnectAsync()
{
- if (_client is { IsConnected: false })
+ if (!_client.IsConnected)
{
- 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
- });
+ await _client.ConnectAsync(_serverEndPoint, _cts.Token);
+ Log($"连接 {_client.LocalEndPoint}{_serverEndPoint} {(_client.IsConnected ? "成功" : "失败")}",
+ _client.IsConnected ? Color.Success : Color.Danger);
}
}
// 3. Extract request/response parsing to utilities:
+private static byte[] BuildRequest()
+{
+ var header = Encoding.UTF8.GetBytes("2025");
+ var body = Encoding.UTF8.GetBytes(DateTime.Now.ToString("ddHHmmss"));
+ return header.Concat(body).ToArray();
+}
+private static (string Text, string Hex) ParseResponse(ReadOnlyMemory<byte> data)
+{
+ var arr = data.ToArray();
+ return (
+ Encoding.UTF8.GetString(arr, 4, arr.Length - 4),
+ BitConverter.ToString(arr)
+ );
+}
// 4. Simplify OnSendAsync:
-private async Task OnSendAsync()
+private async Task OnSendAsync()
{
- if (_client is { IsConnected: true })
+ if (!_client.IsConnected) return;
+
+ var request = BuildRequest();
+ if (!await _client.SendAsync(request, _cts.Token)) return;
+
+ var payload = await _client.ReceiveAsync(_cts.Token);
+ if (payload.IsEmpty) return;
+
+ var (text, hex) = ParseResponse(payload);
+ Log($"接收到来自 {_serverEndPoint} 数据: {text} HEX: {hex}");
}
// 5. Add Log helper:
private void Log(string msg, Color color = default)
{
_items.Add(new ConsoleMessageItem()
{
Message = $"{DateTime.Now}: {msg}",
Color = color
});
}

These steps consolidate your cancellation logic, remove inline buffer manipulation/UI repetition, and keep the socket behavior unchanged.

private readonly CancellationTokenSource _sendTokenSource = new();
private readonly CancellationTokenSource _receiveTokenSource = new();

/// <summary>
/// <inheritdoc/>
/// </summary>
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();
}
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
namespace BootstrapBlazor.Server.Components.Samples.Sockets;

/// <summary>
///
/// 接收电文示例
/// </summary>
public partial class Receives : ComponentBase, IDisposable
public partial class Receives : IDisposable
{
[Inject, NotNull]
private ITcpSocketFactory? TcpSocketFactory { get; set; }
Expand All @@ -19,6 +19,8 @@ public partial class Receives : ComponentBase, IDisposable

private List<ConsoleMessageItem> _items = [];

private readonly IPEndPoint _serverEndPoint = new(IPAddress.Loopback, 8800);

/// <summary>
/// <inheritdoc/>
/// </summary>
Expand All @@ -27,15 +29,18 @@ 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;
}

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);
}
}

Expand Down
20 changes: 7 additions & 13 deletions src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -257,7 +264,6 @@ void AddQuickStar(DemoMenuItem item)
},
new()
{
IsUpdate = true,
Text = Localizer["Labels"],
Url = "label"
},
Expand Down Expand Up @@ -430,7 +436,6 @@ void AddForm(DemoMenuItem item)
},
new()
{
IsNew = true,
Text = Localizer["OtpInput"],
Url = "otp-input"
},
Expand Down Expand Up @@ -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"
},
Expand Down Expand Up @@ -539,7 +542,6 @@ void AddForm(DemoMenuItem item)
},
new()
{
IsNew = true,
Text = Localizer["Vditor"],
Url = "vditor"
}
Expand Down Expand Up @@ -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"
},
Expand Down Expand Up @@ -1241,7 +1241,6 @@ void AddNotice(DemoMenuItem item)
},
new()
{
IsNew = true,
Text = Localizer["FullScreenButton"],
Url = "fullscreen-button"
},
Expand All @@ -1267,7 +1266,6 @@ void AddNotice(DemoMenuItem item)
},
new()
{
IsNew = true,
Text = Localizer["Meet"],
Url = "meet"
},
Expand Down Expand Up @@ -1568,7 +1566,6 @@ void AddServices(DemoMenuItem item)
},
new()
{
IsNew = true,
Text = Localizer["Html2Image"],
Url = "html2image"
},
Expand Down Expand Up @@ -1615,7 +1612,6 @@ void AddServices(DemoMenuItem item)
},
new()
{
IsNew = true,
Text = Localizer["TotpService"],
Url = "otp-service"
},
Expand All @@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ void Invoke(BootstrapBlazorOptions option)
services.AddTaskServices();
services.AddHostedService<ClearTempFilesService>();
services.AddHostedService<MockOnlineContributor>();
services.AddHostedService<MockSocketServerService>();
services.AddHostedService<MockReceiveSocketServerService>();
services.AddHostedService<MockCustomProtocolSocketServerService>();

// 增加通用服务
services.AddBootstrapBlazorServices();
Expand Down
6 changes: 6 additions & 0 deletions src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -7210,5 +7210,11 @@
"OfficeViewerNormalTitle": "Basic Usage",
"OfficeViewerNormalIntro": "Set the document URL for preview by configuring the <code>Url</code> 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 <code>ReceivedCallBack</code> callback method"
}
}
6 changes: 6 additions & 0 deletions src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -7210,5 +7210,11 @@
"OfficeViewerNormalTitle": "基本用法",
"OfficeViewerNormalIntro": "通过设置 <code>Url</code> 值设置预览文档地址",
"OfficeViewerToastSuccessfulContent": "Office 文档加载成功"
},
"BootstrapBlazor.Server.Components.Samples.Sockets.Receives": {
"ReceivesTitle": "Socket 接收示例",
"ReceivesDescription": "通过 Socket 接收数据并且显示",
"NormalTitle": "基本用法",
"NormalIntro": "连接后通过 <code>ReceivedCallBack</code> 回调方法自动接收服务端发送来的时间戳数据"
}
}
Loading