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 @@ -39,7 +39,11 @@
<li>响应头为 4 字节定长,响应体为 8 个字节定长</li>
<li>响应体为字符串类型数据</li>
</ul>
<p>本示例服务器端模拟了数据分包即响应数据实际是两次写入所以实际接收端是要通过两次接收才能得到一个完整的响应数据包,可通过 <b>数据适配器</b> 来简化接收逻辑。通过切换下方 <b>是否使用数据适配器</b> 控制开关进行测试查看实际数据接收情况</p>
<p>本示例服务器端模拟了数据分包即响应数据实际是两次写入所以实际接收端是要通过两次接收才能得到一个完整的响应数据包,可通过 <b>数据适配器</b> 来简化接收逻辑。通过切换下方 <b>是否使用数据适配器</b> 控制开关进行测试查看实际数据接收情况。</p>
<ul class="ul-demo">
<li>不使用 <b>数据处理器</b> 要分两次接收才能接收完整</li>
<li>使用 <b>数据处理器</b> 一次即可接收完整数据包</li>
</ul>
<Pre>private readonly DataPackageAdapter _dataAdapter = new()
{
// 数据适配器内部使用固定长度数据处理器
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@page "/socket/auto-connect"
@inject IStringLocalizer<AutoReconnects> Localizer

<h3>@Localizer["AutoReconnectsTitle"]</h3>
<h4>@Localizer["AutoReconnectsDescription"]</h4>

<Notice></Notice>

<DemoBlock Title="@Localizer["NormalTitle"]"
Introduction="@Localizer["NormalIntro"]"
Name="Normal" ShowCode="false">
<p>本例中模拟自动重连的业务场景,在实际应用中我们可能建立的链路可能由于种种原因断开,所以就有自动重连的业务需求</p>
<p>例如:我们与一个远端节点建立连接后,不停地接收远端发送过来的数据,如果断开连接后需要自动重连后继续接收数据</p>
<p>通过 <code>SocketClientOptions</code> 配置类来开启本功能</p>
<Pre>var client = factory.GetOrCreate("demo-reconnect", op =>
{
op.LocalEndPoint = Utility.ConvertToIpEndPoint("localhost", 0);
options.IsAutoReconnect = true;
options.ReconnectInterval = 5000;
});</Pre>
<p>参数说明:</p>
<ul class="ul-demo">
<li><code>IsAutoReconnect</code> 是否开启自动重连功能</li>
<li><code>ReconnectInterval</code> 自动重连等待间隔 默认 5000 毫秒</li>
</ul>
<p>本例中点击 <b>连接</b> 按钮后程序连接到一个发送数据后自动关闭的模拟服务端,通过输出日志查看运行情况,点击 <code>断开</code> 按钮后程序停止自动重连</p>
<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,109 @@
// 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 AutoReconnects : IDisposable
{
[Inject, NotNull]
private ITcpSocketFactory? TcpSocketFactory { get; set; }

private ITcpSocketClient _client = null!;

private List<ConsoleMessageItem> _items = [];

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

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();

// 从服务中获取 Socket 实例
_client = TcpSocketFactory.GetOrCreate("demo-auto-connect", options =>
{
options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0);
options.IsAutoReconnect = true;
options.ReconnectInterval = 5000;
});
_client.ReceivedCallBack += OnReceivedAsync;
_client.OnConnecting = async () =>
{
_items.Add(new ConsoleMessageItem { Message = $"{DateTime.Now} 正在连接到 {_serverEndPoint},请稍候..." });
await InvokeAsync(StateHasChanged);
};
_client.OnConnected = async () =>
{
_items.Add(new ConsoleMessageItem { Message = $"{DateTime.Now} 已连接到 {_serverEndPoint},等待接收数据", Color = Color.Success });
await InvokeAsync(StateHasChanged);
};
}

private async Task OnConnectAsync()
{
if (_client is { IsConnected: false })
{
await _client.ConnectAsync(_serverEndPoint, 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(data.IsEmpty
? new ConsoleMessageItem { Message = $"{DateTime.Now} 当前连接已关闭,5s 后自动重连", Color = Color.Danger }
: new ConsoleMessageItem { Message = $"{DateTime.Now} 接收到来自站点的数据为 {payload}" });

// 保持队列中最大数量为 50
while (_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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ void AddSocket(DemoMenuItem item)
IsNew = true,
Text = Localizer["DataPackageAdapter"],
Url = "socket/adapter"
},
new()
{
IsNew = true,
Text = Localizer["SocketAutoConnect"],
Url = "socket/auto-connect"
}
};
AddBadge(item, count: 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ void Invoke(BootstrapBlazorOptions option)
services.AddHostedService<MockReceiveSocketServerService>();
services.AddHostedService<MockSendReceiveSocketServerService>();
services.AddHostedService<MockCustomProtocolSocketServerService>();
services.AddHostedService<MockDisconnectServerService>();

// 增加通用服务
services.AddBootstrapBlazorServices();
Expand Down
9 changes: 8 additions & 1 deletion src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -4834,7 +4834,8 @@
"SocketComponents": "ITcpSocketFactory",
"SocketAutoReceive": "Auto Receive",
"SocketManualReceive": "Manual Receive",
"DataPackageAdapter": "DataPackageAdapter"
"DataPackageAdapter": "DataPackageAdapter",
"SocketAutoConnect": "Reconnect"
},
"BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": {
"TablesHeaderTitle": "Header grouping function",
Expand Down Expand Up @@ -7107,5 +7108,11 @@
"AdaptersDescription": "Receive data through the data adapter and display",
"NormalTitle": "Basic usage",
"NormalIntro": "After the connection is established, the timestamp data sent by the server is received through the <code>ReceivedCallBack</code> callback method of the <code>DataPackageAdapter</code> data adapter."
},
"BootstrapBlazor.Server.Components.Samples.Sockets.AutoReconnects": {
"AutoReconnectsTitle": "DataPackageAdapter",
"AutoReconnectsDescription": "Receive data through the data adapter and display",
"NormalTitle": "Basic usage",
"NormalIntro": "Enable automatic reconnection by setting <code>IsAutoReconnect</code>"
}
}
9 changes: 8 additions & 1 deletion src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -4834,7 +4834,8 @@
"SocketComponents": "Socket 服务",
"SocketAutoReceive": "自动接收数据",
"SocketManualReceive": "手动接收数据",
"DataPackageAdapter": "数据处理器"
"DataPackageAdapter": "数据处理器",
"SocketAutoConnect": "自动重连"
},
"BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": {
"TablesHeaderTitle": "表头分组功能",
Expand Down Expand Up @@ -7107,5 +7108,11 @@
"AdaptersDescription": "通过数据适配器接收数据并且显示",
"NormalTitle": "基本用法",
"NormalIntro": "连接后通过 <code>DataPackageAdapter</code> 数据适配器的 <code>ReceivedCallBack</code> 回调方法接收服务端发送来的时间戳数据"
},
"BootstrapBlazor.Server.Components.Samples.Sockets.AutoReconnects": {
"AutoReconnectsTitle": "Socket 自动重连示例",
"AutoReconnectsDescription": "链路断开后自动重连示例",
"NormalTitle": "基本用法",
"NormalIntro": "通过设置 <code>IsAutoReconnect</code> 开启自动重连机制"
}
}
64 changes: 64 additions & 0 deletions src/BootstrapBlazor.Server/Services/MockDisconnectService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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;
using System.Text;

namespace Longbow.Tasks.Services;

/// <summary>
/// 模拟 Socket 自动断开服务端服务类
/// </summary>
internal class MockDisconnectServerService(ILogger<MockDisconnectServerService> logger) : BackgroundService
{
/// <summary>
/// 运行任务
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var server = new TcpListener(IPAddress.Loopback, 8901);
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
{
// 发送数据
await stream.WriteAsync(Encoding.UTF8.GetBytes(DateTime.Now.ToString("yyyyMMddHHmmss")), stoppingToken);
await Task.Delay(2000, stoppingToken);

// 主动关闭连接
client.Close();
}
catch (OperationCanceledException) { break; }
catch (IOException) { break; }
catch (SocketException) { break; }
catch (Exception ex)
{
logger.LogError(ex, "MockDisconnectServerService 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 @@ -245,7 +245,8 @@
"office-viewer": "OfficeViewers",
"socket/manual-receive": "Sockets\\ManualReceives",
"socket/auto-receive": "Sockets\\AutoReceives",
"socket/adapter": "Sockets\\Adapters"
"socket/adapter": "Sockets\\Adapters",
"socket/auto-connect": "Sockets\\AutoReconnects"
},
"video": {
"table": "BV1ap4y1x7Qn?p=1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public async ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken
{
var stream = _client.GetStream();
len = await stream.ReadAsync(buffer, token).ConfigureAwait(false);

if (len == 0)
{
_client.Close();
}
}
return len;
}
Expand Down
Loading
Loading