Skip to content

Commit 5d2ed31

Browse files
authored
feat(ITcpSocketClient): add OnConnecting callback (#6392)
* doc: 增加数据处理器文档说明 * doc: 增加自动重连示例 * doc: 增加自动重连菜单 * feat: 增加 Connect 连接回调 * feat: 拆分自动重连逻辑 * feat: 远端关闭后销毁 TcpClient 实例 * doc: 增加重连示例 * test: 增加 OnConnect 回调单元测试 * refactor: 更新日志开启逻辑 * test: 增加服务器断开单元测试 * doc: 增加自动重连菜单 * doc: 增加文件映射 * doc: 更新自动重连文档 * refactor: 删除样式文件
1 parent 39578ed commit 5d2ed31

File tree

14 files changed

+381
-28
lines changed

14 files changed

+381
-28
lines changed

src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@
3939
<li>响应头为 4 字节定长,响应体为 8 个字节定长</li>
4040
<li>响应体为字符串类型数据</li>
4141
</ul>
42-
<p>本示例服务器端模拟了数据分包即响应数据实际是两次写入所以实际接收端是要通过两次接收才能得到一个完整的响应数据包,可通过 <b>数据适配器</b> 来简化接收逻辑。通过切换下方 <b>是否使用数据适配器</b> 控制开关进行测试查看实际数据接收情况</p>
42+
<p>本示例服务器端模拟了数据分包即响应数据实际是两次写入所以实际接收端是要通过两次接收才能得到一个完整的响应数据包,可通过 <b>数据适配器</b> 来简化接收逻辑。通过切换下方 <b>是否使用数据适配器</b> 控制开关进行测试查看实际数据接收情况。</p>
43+
<ul class="ul-demo">
44+
<li>不使用 <b>数据处理器</b> 要分两次接收才能接收完整</li>
45+
<li>使用 <b>数据处理器</b> 一次即可接收完整数据包</li>
46+
</ul>
4347
<Pre>private readonly DataPackageAdapter _dataAdapter = new()
4448
{
4549
// 数据适配器内部使用固定长度数据处理器
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
@page "/socket/auto-connect"
2+
@inject IStringLocalizer<AutoReconnects> Localizer
3+
4+
<h3>@Localizer["AutoReconnectsTitle"]</h3>
5+
<h4>@Localizer["AutoReconnectsDescription"]</h4>
6+
7+
<Notice></Notice>
8+
9+
<DemoBlock Title="@Localizer["NormalTitle"]"
10+
Introduction="@Localizer["NormalIntro"]"
11+
Name="Normal" ShowCode="false">
12+
<p>本例中模拟自动重连的业务场景,在实际应用中我们可能建立的链路可能由于种种原因断开,所以就有自动重连的业务需求</p>
13+
<p>例如:我们与一个远端节点建立连接后,不停地接收远端发送过来的数据,如果断开连接后需要自动重连后继续接收数据</p>
14+
<p>通过 <code>SocketClientOptions</code> 配置类来开启本功能</p>
15+
<Pre>var client = factory.GetOrCreate("demo-reconnect", op =>
16+
{
17+
op.LocalEndPoint = Utility.ConvertToIpEndPoint("localhost", 0);
18+
options.IsAutoReconnect = true;
19+
options.ReconnectInterval = 5000;
20+
});</Pre>
21+
<p>参数说明:</p>
22+
<ul class="ul-demo">
23+
<li><code>IsAutoReconnect</code> 是否开启自动重连功能</li>
24+
<li><code>ReconnectInterval</code> 自动重连等待间隔 默认 5000 毫秒</li>
25+
</ul>
26+
<p>本例中点击 <b>连接</b> 按钮后程序连接到一个发送数据后自动关闭的模拟服务端,通过输出日志查看运行情况,点击 <code>断开</code> 按钮后程序停止自动重连</p>
27+
<div class="row form-inline g-3">
28+
<div class="col-12 col-sm-6">
29+
<Button Text="连接" Icon="fa-solid fa-play"
30+
OnClick="OnConnectAsync" IsDisabled="@_client.IsConnected"></Button>
31+
<Button Text="断开" Icon="fa-solid fa-stop" class="ms-2"
32+
OnClick="OnCloseAsync" IsDisabled="@(!_client.IsConnected)"></Button>
33+
</div>
34+
<div class="col-12">
35+
<Console Items="@_items" Height="496" HeaderText="接收数据(间隔 10 秒)"
36+
ShowAutoScroll="true" OnClear="@OnClear"></Console>
37+
</div>
38+
</div>
39+
</DemoBlock>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
using System.Net;
7+
8+
namespace BootstrapBlazor.Server.Components.Samples.Sockets;
9+
10+
/// <summary>
11+
/// 自动重连示例组件
12+
/// </summary>
13+
public partial class AutoReconnects : IDisposable
14+
{
15+
[Inject, NotNull]
16+
private ITcpSocketFactory? TcpSocketFactory { get; set; }
17+
18+
private ITcpSocketClient _client = null!;
19+
20+
private List<ConsoleMessageItem> _items = [];
21+
22+
private readonly IPEndPoint _serverEndPoint = new(IPAddress.Loopback, 8901);
23+
24+
/// <summary>
25+
/// <inheritdoc/>
26+
/// </summary>
27+
protected override void OnInitialized()
28+
{
29+
base.OnInitialized();
30+
31+
// 从服务中获取 Socket 实例
32+
_client = TcpSocketFactory.GetOrCreate("demo-auto-connect", options =>
33+
{
34+
options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0);
35+
options.IsAutoReconnect = true;
36+
options.ReconnectInterval = 5000;
37+
});
38+
_client.ReceivedCallBack += OnReceivedAsync;
39+
_client.OnConnecting = async () =>
40+
{
41+
_items.Add(new ConsoleMessageItem { Message = $"{DateTime.Now} 正在连接到 {_serverEndPoint},请稍候..." });
42+
await InvokeAsync(StateHasChanged);
43+
};
44+
_client.OnConnected = async () =>
45+
{
46+
_items.Add(new ConsoleMessageItem { Message = $"{DateTime.Now} 已连接到 {_serverEndPoint},等待接收数据", Color = Color.Success });
47+
await InvokeAsync(StateHasChanged);
48+
};
49+
}
50+
51+
private async Task OnConnectAsync()
52+
{
53+
if (_client is { IsConnected: false })
54+
{
55+
await _client.ConnectAsync(_serverEndPoint, CancellationToken.None);
56+
}
57+
}
58+
59+
private async Task OnCloseAsync()
60+
{
61+
if (_client is { IsConnected: true })
62+
{
63+
await _client.CloseAsync();
64+
}
65+
}
66+
67+
private Task OnClear()
68+
{
69+
_items = [];
70+
return Task.CompletedTask;
71+
}
72+
73+
private async ValueTask OnReceivedAsync(ReadOnlyMemory<byte> data)
74+
{
75+
// 将数据显示为十六进制字符串
76+
var payload = System.Text.Encoding.UTF8.GetString(data.Span);
77+
_items.Add(data.IsEmpty
78+
? new ConsoleMessageItem { Message = $"{DateTime.Now} 当前连接已关闭,5s 后自动重连", Color = Color.Danger }
79+
: new ConsoleMessageItem { Message = $"{DateTime.Now} 接收到来自站点的数据为 {payload}" });
80+
81+
// 保持队列中最大数量为 50
82+
while (_items.Count > 50)
83+
{
84+
_items.RemoveAt(0);
85+
}
86+
87+
await InvokeAsync(StateHasChanged);
88+
}
89+
90+
private void Dispose(bool disposing)
91+
{
92+
if (disposing)
93+
{
94+
if (_client is { IsConnected: true })
95+
{
96+
_client.ReceivedCallBack -= OnReceivedAsync;
97+
}
98+
}
99+
}
100+
101+
/// <summary>
102+
/// <inheritdoc/>
103+
/// </summary>
104+
public void Dispose()
105+
{
106+
Dispose(true);
107+
GC.SuppressFinalize(this);
108+
}
109+
}

src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,12 @@ void AddSocket(DemoMenuItem item)
224224
IsNew = true,
225225
Text = Localizer["DataPackageAdapter"],
226226
Url = "socket/adapter"
227+
},
228+
new()
229+
{
230+
IsNew = true,
231+
Text = Localizer["SocketAutoConnect"],
232+
Url = "socket/auto-connect"
227233
}
228234
};
229235
AddBadge(item, count: 1);

src/BootstrapBlazor.Server/Extensions/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ void Invoke(BootstrapBlazorOptions option)
4848
services.AddHostedService<MockReceiveSocketServerService>();
4949
services.AddHostedService<MockSendReceiveSocketServerService>();
5050
services.AddHostedService<MockCustomProtocolSocketServerService>();
51+
services.AddHostedService<MockDisconnectServerService>();
5152

5253
// 增加通用服务
5354
services.AddBootstrapBlazorServices();

src/BootstrapBlazor.Server/Locales/en-US.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4834,7 +4834,8 @@
48344834
"SocketComponents": "ITcpSocketFactory",
48354835
"SocketAutoReceive": "Auto Receive",
48364836
"SocketManualReceive": "Manual Receive",
4837-
"DataPackageAdapter": "DataPackageAdapter"
4837+
"DataPackageAdapter": "DataPackageAdapter",
4838+
"SocketAutoConnect": "Reconnect"
48384839
},
48394840
"BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": {
48404841
"TablesHeaderTitle": "Header grouping function",
@@ -7107,5 +7108,11 @@
71077108
"AdaptersDescription": "Receive data through the data adapter and display",
71087109
"NormalTitle": "Basic usage",
71097110
"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."
7111+
},
7112+
"BootstrapBlazor.Server.Components.Samples.Sockets.AutoReconnects": {
7113+
"AutoReconnectsTitle": "DataPackageAdapter",
7114+
"AutoReconnectsDescription": "Receive data through the data adapter and display",
7115+
"NormalTitle": "Basic usage",
7116+
"NormalIntro": "Enable automatic reconnection by setting <code>IsAutoReconnect</code>"
71107117
}
71117118
}

src/BootstrapBlazor.Server/Locales/zh-CN.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4834,7 +4834,8 @@
48344834
"SocketComponents": "Socket 服务",
48354835
"SocketAutoReceive": "自动接收数据",
48364836
"SocketManualReceive": "手动接收数据",
4837-
"DataPackageAdapter": "数据处理器"
4837+
"DataPackageAdapter": "数据处理器",
4838+
"SocketAutoConnect": "自动重连"
48384839
},
48394840
"BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": {
48404841
"TablesHeaderTitle": "表头分组功能",
@@ -7107,5 +7108,11 @@
71077108
"AdaptersDescription": "通过数据适配器接收数据并且显示",
71087109
"NormalTitle": "基本用法",
71097110
"NormalIntro": "连接后通过 <code>DataPackageAdapter</code> 数据适配器的 <code>ReceivedCallBack</code> 回调方法接收服务端发送来的时间戳数据"
7111+
},
7112+
"BootstrapBlazor.Server.Components.Samples.Sockets.AutoReconnects": {
7113+
"AutoReconnectsTitle": "Socket 自动重连示例",
7114+
"AutoReconnectsDescription": "链路断开后自动重连示例",
7115+
"NormalTitle": "基本用法",
7116+
"NormalIntro": "通过设置 <code>IsAutoReconnect</code> 开启自动重连机制"
71107117
}
71117118
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
using System.Net;
7+
using System.Net.Sockets;
8+
using System.Text;
9+
10+
namespace Longbow.Tasks.Services;
11+
12+
/// <summary>
13+
/// 模拟 Socket 自动断开服务端服务类
14+
/// </summary>
15+
internal class MockDisconnectServerService(ILogger<MockDisconnectServerService> logger) : BackgroundService
16+
{
17+
/// <summary>
18+
/// 运行任务
19+
/// </summary>
20+
/// <param name="stoppingToken"></param>
21+
/// <returns></returns>
22+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
23+
{
24+
var server = new TcpListener(IPAddress.Loopback, 8901);
25+
server.Start();
26+
while (stoppingToken is { IsCancellationRequested: false })
27+
{
28+
try
29+
{
30+
var client = await server.AcceptTcpClientAsync(stoppingToken);
31+
_ = Task.Run(() => OnDataHandlerAsync(client, stoppingToken), stoppingToken);
32+
}
33+
catch { }
34+
}
35+
}
36+
37+
private async Task OnDataHandlerAsync(TcpClient client, CancellationToken stoppingToken)
38+
{
39+
// 方法目的:
40+
// 收到消息后发送自定义通讯协议的响应数据
41+
// 响应头 + 响应体
42+
await using var stream = client.GetStream();
43+
while (stoppingToken is { IsCancellationRequested: false })
44+
{
45+
try
46+
{
47+
// 发送数据
48+
await stream.WriteAsync(Encoding.UTF8.GetBytes(DateTime.Now.ToString("yyyyMMddHHmmss")), stoppingToken);
49+
await Task.Delay(2000, stoppingToken);
50+
51+
// 主动关闭连接
52+
client.Close();
53+
}
54+
catch (OperationCanceledException) { break; }
55+
catch (IOException) { break; }
56+
catch (SocketException) { break; }
57+
catch (Exception ex)
58+
{
59+
logger.LogError(ex, "MockDisconnectServerService encountered an error while sending data.");
60+
break;
61+
}
62+
}
63+
}
64+
}

src/BootstrapBlazor.Server/docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@
245245
"office-viewer": "OfficeViewers",
246246
"socket/manual-receive": "Sockets\\ManualReceives",
247247
"socket/auto-receive": "Sockets\\AutoReceives",
248-
"socket/adapter": "Sockets\\Adapters"
248+
"socket/adapter": "Sockets\\Adapters",
249+
"socket/auto-connect": "Sockets\\AutoReconnects"
249250
},
250251
"video": {
251252
"table": "BV1ap4y1x7Qn?p=1",

src/BootstrapBlazor/Services/TcpSocket/DefaultSocketClientProvider.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public async ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken
6969
{
7070
var stream = _client.GetStream();
7171
len = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
72+
73+
if (len == 0)
74+
{
75+
_client.Close();
76+
}
7277
}
7378
return len;
7479
}

0 commit comments

Comments
 (0)