Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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 @@ -6,13 +6,15 @@

<p class="code-label">1. 服务注入</p>

<Pre>[Inject]
[NotNull]
private ITcpSocketFactory? TcpSocketFactory { get; set; }</Pre>
<Pre>services.AddBootstrapBlazorTcpSocketFactory();</Pre>

<p class="code-label">2. 使用服务</p>
<p>调用 <code>TcpSocketFactory</code> 实例方法 <code>GetOrCreate</code> 即可得到一个 <code>ITcpSocketClient</code> 实例。内部提供复用机制,调用两次得到的 <code>ITcpSocketClient</code> 为同一对象</p>

<Pre>[Inject]
[NotNull]
private ITcpSocketFactory? TcpSocketFactory { get; set; }</Pre>

<Pre>var client = factory.GetOrCreate("192.168.1.100", 0);</Pre>

<p class="code-label">3. 使用方法</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Tips>
<p><code>ITcpSocketFactory</code> 服务仅在 <code>Server</code> 模式下可用</p>
</Tips>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@page "/socket/receive"
@inject IStringLocalizer<Receives> Localizer

<h3>@Localizer["ReceivesTitle"]</h3>
<h4>@Localizer["ReceivesDescription"]</h4>

<Notice></Notice>

<DemoBlock Title="@Localizer["NormalTitle"]"
Introduction="@Localizer["NormalIntro"]"
Name="Normal" ShowCode="false">
<p>本例中连接一个模拟时间同步服务,每间隔 10s 自动发送服务器时间戳,连接后无需发送任何数据即可持续收到时间戳数据</p>
<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>
</ul>
<p>使用 <code>ReceivedCallBack</code> 委托获得接收到的数据,可通过 <code>+=</code> 方法支持多个客户端接收数据</p>
<Pre>_client.ReceivedCallBack += OnReceivedAsync;</Pre>
<p>特别注意页面需要继承 <code>IDisposable</code> 或者 <code>IAsyncDisposable</code> 接口,在 <code>Dispose</code> 或者 <code>DisposeAsync</code> 中移除委托</p>
<Pre>private void Dispose(bool disposing)
{
if (disposing)
{
if (_client is { IsConnected: true })
{
_client.ReceivedCallBack -= OnReceivedAsync;
}
}
}</Pre>

<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>
</div>
<div class="col-12">
<Console Items="@_items" Height="496" HeaderText="接收数据(间隔 10 秒)"
ShowAutoScroll="true" OnClear="@OnClear"></Console>
</div>
</div>
</DemoBlock>

Original file line number Diff line number Diff line change
@@ -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([email protected]) Website: https://www.blazor.zone

using System.Net;

namespace BootstrapBlazor.Server.Components.Samples.Sockets;

/// <summary>
///
/// </summary>
public partial class Receives : ComponentBase, IDisposable
{
[Inject, NotNull]
private ITcpSocketFactory? TcpSocketFactory { get; set; }

private ITcpSocketClient _client = null!;

private List<ConsoleMessageItem> _items = [];

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

/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
20 changes: 20 additions & 0 deletions src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ public static List<MenuItem> GenerateMenus(this IStringLocalizer<NavMenu> 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"],
Expand Down Expand Up @@ -205,6 +212,19 @@ void AddSpeech(DemoMenuItem item)
AddBadge(item, count: 5);
}

void AddSocket(DemoMenuItem item)
{
item.Items = new List<DemoMenuItem>
{
new()
{
Text = Localizer["SocketReceive"],
Url = "socket/receive"
}
};
AddBadge(item, count: 1);
}

void AddQuickStar(DemoMenuItem item)
{
item.Items = new List<DemoMenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ void Invoke(BootstrapBlazorOptions option)
services.AddTaskServices();
services.AddHostedService<ClearTempFilesService>();
services.AddHostedService<MockOnlineContributor>();
services.AddHostedService<MockSocketServerService>();

// 增加通用服务
services.AddBootstrapBlazorServices();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ public static IServiceCollection AddBootstrapBlazorServices(this IServiceCollect
// 增加 JuHe 定位服务
services.AddBootstrapBlazorJuHeIpLocatorService();

// 增加 ITcpSocketFactory 服务
services.AddBootstrapBlazorTcpSocketFactory();

// 增加 PetaPoco ORM 数据服务操作类
// 需要时打开下面代码
//services.AddPetaPoco(option =>
Expand Down
4 changes: 3 additions & 1 deletion src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "表头分组功能",
Expand Down
60 changes: 60 additions & 0 deletions src/BootstrapBlazor.Server/Services/MockSocketServerService.cs
Original file line number Diff line number Diff line change
@@ -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([email protected]) Website: https://www.blazor.zone

using System.Net;
using System.Net.Sockets;

namespace Longbow.Tasks.Services;

/// <summary>
/// 模拟 Socket 服务端服务类
/// </summary>
internal class MockSocketServerService(ILogger<MockSocketServerService> logger) : BackgroundService
{
/// <summary>
/// 运行任务
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
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;
}
}
}
}
3 changes: 2 additions & 1 deletion src/BootstrapBlazor.Server/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>9.8.0-beta06</Version>
<Version>9.8.0-beta07</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading