-
-
Notifications
You must be signed in to change notification settings - Fork 363
doc(ITcpSocketClient): add DataHandler sample code #6331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Reviewer's GuideThis PR introduces a new DataAdapter sample under Sockets, refactors the existing Receives sample to use configurable endpoints, updates menu localization flags, renames and adds mock socket services, adjusts the TcpSocketClientBase logging method, and registers new services for custom-protocol demos. Class diagram for new Adapters socket sample and related servicesclassDiagram
class Adapters {
- ITcpSocketFactory TcpSocketFactory
- ITcpSocketClient _client
- List~ConsoleMessageItem~ _items
- IPEndPoint _serverEndPoint
- CancellationTokenSource _connectTokenSource
- CancellationTokenSource _sendTokenSource
- CancellationTokenSource _receiveTokenSource
+ OnInitialized()
+ OnConnectAsync()
+ OnSendAsync()
+ OnCloseAsync()
+ OnClear()
+ Dispose(bool disposing)
+ Dispose()
}
class MockCustomProtocolSocketServerService {
+ ExecuteAsync(CancellationToken stoppingToken)
- OnDataHandlerAsync(TcpClient client, CancellationToken stoppingToken)
}
class MockReceiveSocketServerService {
+ ExecuteAsync(CancellationToken stoppingToken)
- MockSendAsync(TcpClient client, CancellationToken stoppingToken)
}
class TcpSocketClientBase {
# ISocketClientProvider SocketClientProvider
# ILogger Logger
+ Log(LogLevel logLevel, Exception? ex, string? message)
}
Adapters --|> IDisposable
MockCustomProtocolSocketServerService --|> BackgroundService
MockReceiveSocketServerService --|> BackgroundService
TcpSocketClientBase <|.. ITcpSocketClient
Adapters --> ITcpSocketFactory
Adapters --> ITcpSocketClient
Adapters --> ConsoleMessageItem
Adapters --> IPEndPoint
Adapters --> CancellationTokenSource
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @ArgoZhang - I've reviewed your changes - here's some feedback:
- The Receives component removed ComponentBase but still overrides OnInitialized and depends on Blazor lifecycle—please restore ComponentBase inheritance so lifecycle methods work correctly.
- Both Receives and Adapters implement IDisposable but never call CloseAsync or dispose the underlying _client—ensure the TCP client is properly closed or disposed in Dispose to avoid resource leaks.
- You’ve registered two mock socket server hosted services unconditionally; consider only adding them in a development or testing environment (e.g. via IConfiguration or environment checks) to prevent them running in production.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The Receives component removed ComponentBase but still overrides OnInitialized and depends on Blazor lifecycle—please restore ComponentBase inheritance so lifecycle methods work correctly.
- Both Receives and Adapters implement IDisposable but never call CloseAsync or dispose the underlying _client—ensure the TCP client is properly closed or disposed in Dispose to avoid resource leaks.
- You’ve registered two mock socket server hosted services unconditionally; consider only adding them in a development or testing environment (e.g. via IConfiguration or environment checks) to prevent them running in production.
## Individual Comments
### Comment 1
<location> `src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs:14` </location>
<code_context>
+/// <summary>
+/// 模拟 Socket 服务端服务类
+/// </summary>
+internal class MockCustomProtocolSocketServerService(ILogger<MockCustomProtocolSocketServerService> logger) : BackgroundService
+{
+ /// <summary>
</code_context>
<issue_to_address>
Typo in file/class name: 'Protocal' should be 'Protocol'.
Please rename to 'MockCustomProtocolSocketServerService' for clarity and consistency.
Suggested implementation:
```csharp
internal class MockCustomProtocolSocketServerService(ILogger<MockCustomProtocolSocketServerService> logger) : BackgroundService
```
1. Rename the file from `MockCustomProtocalSocketServerService.cs` to `MockCustomProtocolSocketServerService.cs`.
2. Update any references to `MockCustomProtocalSocketServerService` elsewhere in the codebase to use `MockCustomProtocolSocketServerService`.
</issue_to_address>
### Comment 2
<location> `src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs:23` </location>
<code_context>
+ /// <returns></returns>
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ var server = new TcpListener(IPAddress.Loopback, 8900);
+ server.Start();
+ while (stoppingToken is { IsCancellationRequested: false })
</code_context>
<issue_to_address>
TcpListener is not disposed.
Call server.Stop() in a finally block or when cancellation is requested to ensure the port and resources are released properly.
</issue_to_address>
### Comment 3
<location> `src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs:29` </location>
<code_context>
+ {
+ try
+ {
+ var client = await server.AcceptTcpClientAsync(stoppingToken);
+ _ = Task.Run(() => OnDataHandlerAsync(client, stoppingToken), stoppingToken);
+ }
</code_context>
<issue_to_address>
TcpClient is not disposed after use.
Ensure TcpClient is disposed after OnDataHandlerAsync completes to prevent resource leaks.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
var client = await server.AcceptTcpClientAsync(stoppingToken);
_ = Task.Run(() => OnDataHandlerAsync(client, stoppingToken), stoppingToken);
=======
var client = await server.AcceptTcpClientAsync(stoppingToken);
_ = Task.Run(async () =>
{
try
{
await OnDataHandlerAsync(client, stoppingToken);
}
finally
{
client.Dispose();
}
}, stoppingToken);
>>>>>>> REPLACE
</suggested_fix>
### Comment 4
<location> `src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs:47` </location>
<code_context>
+ try
+ {
+ // 接收数据
+ var len = await stream.ReadAsync(new byte[1024], stoppingToken);
+ if (len == 0)
+ {
</code_context>
<issue_to_address>
Allocating a new buffer on every read is inefficient.
Allocate the buffer once before the loop and reuse it to improve performance.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
await using var stream = client.GetStream();
while (stoppingToken is { IsCancellationRequested: false })
{
try
{
// 接收数据
var len = await stream.ReadAsync(new byte[1024], stoppingToken);
if (len == 0)
{
=======
await using var stream = client.GetStream();
var buffer = new byte[1024];
while (stoppingToken is { IsCancellationRequested: false })
{
try
{
// 接收数据
var len = await stream.ReadAsync(buffer, stoppingToken);
if (len == 0)
{
>>>>>>> REPLACE
</suggested_fix>
### Comment 5
<location> `src/BootstrapBlazor.Server/Services/MockCustomProtocalSocketServerService.cs:60` </location>
<code_context>
+ // 响应头: 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);
</code_context>
<issue_to_address>
Unnecessary ToArray() call on u8 string literal.
CopyTo can be called directly on the ReadOnlySpan<byte> from the u8 literal: use "2025"u8.CopyTo(data) for better clarity and efficiency.
</issue_to_address>
### Comment 6
<location> `src/BootstrapBlazor.Server/Components/Samples/Sockets/Adapters.razor.cs:26` </location>
<code_context>
+
+ private readonly IPEndPoint _serverEndPoint = new(IPAddress.Loopback, 8900);
+
+ private readonly CancellationTokenSource _connectTokenSource = new();
+ private readonly CancellationTokenSource _sendTokenSource = new();
+ private readonly CancellationTokenSource _receiveTokenSource = new();
</code_context>
<issue_to_address>
Consider consolidating cancellation logic and extracting repeated code into helpers to simplify and clarify the component.
```suggestion
// 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.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| /// <summary> | ||
| /// 模拟 Socket 服务端服务类 | ||
| /// </summary> | ||
| internal class MockCustomProtocolSocketServerService(ILogger<MockCustomProtocolSocketServerService> logger) : BackgroundService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (typo): Typo in file/class name: 'Protocal' should be 'Protocol'.
Please rename to 'MockCustomProtocolSocketServerService' for clarity and consistency.
Suggested implementation:
internal class MockCustomProtocolSocketServerService(ILogger<MockCustomProtocolSocketServerService> logger) : BackgroundService- Rename the file from
MockCustomProtocalSocketServerService.cstoMockCustomProtocolSocketServerService.cs. - Update any references to
MockCustomProtocalSocketServerServiceelsewhere in the codebase to useMockCustomProtocolSocketServerService.
| /// <returns></returns> | ||
| protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
| { | ||
| var server = new TcpListener(IPAddress.Loopback, 8900); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): TcpListener is not disposed.
Call server.Stop() in a finally block or when cancellation is requested to ensure the port and resources are released properly.
| var client = await server.AcceptTcpClientAsync(stoppingToken); | ||
| _ = Task.Run(() => OnDataHandlerAsync(client, stoppingToken), stoppingToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): TcpClient is not disposed after use.
Ensure TcpClient is disposed after OnDataHandlerAsync completes to prevent resource leaks.
| var client = await server.AcceptTcpClientAsync(stoppingToken); | |
| _ = Task.Run(() => OnDataHandlerAsync(client, stoppingToken), stoppingToken); | |
| var client = await server.AcceptTcpClientAsync(stoppingToken); | |
| _ = Task.Run(async () => | |
| { | |
| try | |
| { | |
| await OnDataHandlerAsync(client, stoppingToken); | |
| } | |
| finally | |
| { | |
| client.Dispose(); | |
| } | |
| }, 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) | ||
| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (performance): Allocating a new buffer on every read is inefficient.
Allocate the buffer once before the loop and reuse it to improve performance.
| await using var stream = client.GetStream(); | |
| while (stoppingToken is { IsCancellationRequested: false }) | |
| { | |
| try | |
| { | |
| // 接收数据 | |
| var len = await stream.ReadAsync(new byte[1024], stoppingToken); | |
| if (len == 0) | |
| { | |
| await using var stream = client.GetStream(); | |
| var buffer = new byte[1024]; | |
| while (stoppingToken is { IsCancellationRequested: false }) | |
| { | |
| try | |
| { | |
| // 接收数据 | |
| var len = await stream.ReadAsync(buffer, stoppingToken); | |
| if (len == 0) | |
| { |
| // 响应头: 4 字节表示响应体长度 [0x32, 0x30, 0x32, 0x35] | ||
| // 响应体: 8 字节当前时间戳字符串 | ||
| var data = new byte[12]; | ||
| "2025"u8.ToArray().CopyTo(data, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: Unnecessary ToArray() call on u8 string literal.
CopyTo can be called directly on the ReadOnlySpan from the u8 literal: use "2025"u8.CopyTo(data) for better clarity and efficiency.
|
|
||
| private readonly IPEndPoint _serverEndPoint = new(IPAddress.Loopback, 8900); | ||
|
|
||
| private readonly CancellationTokenSource _connectTokenSource = new(); |
There was a problem hiding this comment.
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.
| 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.
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #6331 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 717 717
Lines 31558 31558
Branches 4457 4457
=========================================
Hits 31558 31558
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Link issues
fixes #6330
Summary By Copilot
Regression?
Risk
Verification
Packaging changes reviewed?
☑️ Self Check before Merge
Summary by Sourcery
Add a new DataAdapter sample demonstrating custom protocol handling with ITcpSocketClient, update related socket samples to use options-based configuration, introduce dedicated mock server services for receive and adapter scenarios, and refresh menu entries and extensibility points.
New Features:
Enhancements:
Documentation:
Chores: