Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
@@ -0,0 +1,80 @@
// 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.Buffers;
using System.Text;

namespace BootstrapBlazor.Components;

/// <summary>
/// Handles data packages that are delimited by a specific sequence of bytes or characters.
/// </summary>
/// <remarks>This class provides functionality for processing data packages that are separated by a defined
/// delimiter. The delimiter can be specified as a string with an optional encoding or as a byte array.</remarks>
public class DelimiterDataPackageHandler : DataPackageHandlerBase
{
private readonly ReadOnlyMemory<byte> _delimiter;

/// <summary>
/// Initializes a new instance of the <see cref="DelimiterDataPackageHandler"/> class with the specified delimiter
/// and optional encoding.
/// </summary>
/// <param name="delimiter">The string delimiter used to separate data packages. This value cannot be null or empty.</param>
/// <param name="encoding">The character encoding used to convert the delimiter to bytes. If null, <see cref="Encoding.UTF8"/> is used as
/// the default.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="delimiter"/> is null or empty.</exception>
public DelimiterDataPackageHandler(string delimiter, Encoding? encoding = null)
{

Check warning on line 29 in src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs

View check run for this annotation

Codecov / codecov/patch

src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs#L28-L29

Added lines #L28 - L29 were not covered by tests
if (string.IsNullOrEmpty(delimiter))
{
throw new ArgumentNullException(nameof(delimiter), "Delimiter cannot be null or empty.");

Check warning on line 32 in src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs

View check run for this annotation

Codecov / codecov/patch

src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs#L31-L32

Added lines #L31 - L32 were not covered by tests
}

encoding ??= Encoding.UTF8;
_delimiter = encoding.GetBytes(delimiter);
}

Check warning on line 37 in src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs

View check run for this annotation

Codecov / codecov/patch

src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs#L36-L37

Added lines #L36 - L37 were not covered by tests

/// <summary>
/// Initializes a new instance of the <see cref="DelimiterDataPackageHandler"/> class with the specified delimiters.
/// </summary>
/// <param name="delimiter">An array of bytes representing the delimiters used to parse data packages. Cannot be <see langword="null"/>.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="delimiter"/> is <see langword="null"/>.</exception>
public DelimiterDataPackageHandler(byte[] delimiter)
{
_delimiter = delimiter ?? throw new ArgumentNullException(nameof(delimiter), "Delimiter cannot be null.");
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public override async Task ReceiveAsync(Memory<byte> data)
{
data = ConcatBuffer(data);

var index = data.Span.IndexOfAny(_delimiter.Span);
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Using IndexOfAny with a multi-byte delimiter may not match the intended delimiter sequence.

IndexOfAny matches any single byte from the delimiter, which can cause incorrect splits if the delimiter is multi-byte. Use a method that searches for the full delimiter sequence to ensure correct segmentation.

var segment = index == -1 ? data : data[..index];

var length = segment.Length + _delimiter.Length;
using var buffer = MemoryPool<byte>.Shared.Rent(length);
segment.CopyTo(buffer.Memory);

if (index != -1)
{
SlicePackage(data, index + _delimiter.Length);

_delimiter.CopyTo(buffer.Memory[index..]);
if (ReceivedCallBack != null)
{
await ReceivedCallBack(buffer.Memory[..length].ToArray());
}
}
else
{
SlicePackage(data, 0);
}

Check warning on line 78 in src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs

View check run for this annotation

Codecov / codecov/patch

src/BootstrapBlazor/Services/TcpSocket/DataPackage/DelimiterDataPackageHandler.cs#L76-L78

Added lines #L76 - L78 were not covered by tests
}
}
64 changes: 62 additions & 2 deletions test/UnitTest/Services/TcpSocketFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public async Task CloseByRemote_Ok()
[Fact]
public async Task FixLengthDataPackageHandler_Ok()
{
var port = 8888;
var port = 8884;
var server = StartTcpServer(port, MockSplitPackageAsync);
var client = CreateClient();

Expand Down Expand Up @@ -219,7 +219,7 @@ public async Task FixLengthDataPackageHandler_Ok()
[Fact]
public async Task FixLengthDataPackageHandler_Sticky()
{
var port = 8899;
var port = 8885;
var server = StartTcpServer(port, MockStickyPackageAsync);
var client = CreateClient();

Expand Down Expand Up @@ -263,6 +263,45 @@ public async Task FixLengthDataPackageHandler_Sticky()
StopTcpServer(server);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Missing edge case tests for DelimiterDataPackageHandler.

Please add tests for these edge cases: missing delimiter (data should be buffered), multiple packages in one read (callback for each), delimiter split across reads (correct reassembly), empty payloads (delimiter first), and consecutive delimiters (empty package between). This will improve robustness.

}

[Fact]
public async Task DelimiterDataPackageHandler_Ok()
{
var port = 8886;
var server = StartTcpServer(port, MockDelimiterPackageAsync);
var client = CreateClient();

// 连接 TCP Server
var connect = await client.ConnectAsync("localhost", port);

var tcs = new TaskCompletionSource();
Memory<byte> receivedBuffer = Memory<byte>.Empty;

// 增加数据库处理适配器
client.SetDataHandler(new DelimiterDataPackageHandler(new byte[] { 0x13, 0x10 })
{
ReceivedCallBack = buffer =>
{
receivedBuffer = buffer;
tcs.SetResult();
return Task.CompletedTask;
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): No test for string-based delimiter constructor.

Please add tests for the string-based constructor, both with and without the encoding parameter, to ensure full coverage.

Suggested change
// 增加数据库处理适配器
client.SetDataHandler(new DelimiterDataPackageHandler(new byte[] { 0x13, 0x10 })
{
ReceivedCallBack = buffer =>
{
receivedBuffer = buffer;
tcs.SetResult();
return Task.CompletedTask;
}
});
// 增加数据库处理适配器
client.SetDataHandler(new DelimiterDataPackageHandler(new byte[] { 0x13, 0x10 })
{
ReceivedCallBack = buffer =>
{
receivedBuffer = buffer;
tcs.SetResult();
return Task.CompletedTask;
}
});
// Test: string-based delimiter constructor (default encoding)
var tcsStringDelimiter = new TaskCompletionSource();
Memory<byte> receivedBufferStringDelimiter = Memory<byte>.Empty;
var stringDelimiterHandler = new DelimiterDataPackageHandler("\r\n")
{
ReceivedCallBack = buffer =>
{
receivedBufferStringDelimiter = buffer;
tcsStringDelimiter.SetResult();
return Task.CompletedTask;
}
};
client.SetDataHandler(stringDelimiterHandler);
await client.SendAsync(data);
await tcsStringDelimiter.Task;
Assert.Equal(data.ToArray(), receivedBufferStringDelimiter.ToArray());
// Test: string-based delimiter constructor (with encoding)
var tcsStringDelimiterEncoding = new TaskCompletionSource();
Memory<byte> receivedBufferStringDelimiterEncoding = Memory<byte>.Empty;
var stringDelimiterHandlerEncoding = new DelimiterDataPackageHandler("\r\n", System.Text.Encoding.UTF8)
{
ReceivedCallBack = buffer =>
{
receivedBufferStringDelimiterEncoding = buffer;
tcsStringDelimiterEncoding.SetResult();
return Task.CompletedTask;
}
};
client.SetDataHandler(stringDelimiterHandlerEncoding);
await client.SendAsync(data);
await tcsStringDelimiterEncoding.Task;
Assert.Equal(data.ToArray(), receivedBufferStringDelimiterEncoding.ToArray());


// 发送数据
var data = new Memory<byte>([1, 2, 3, 4, 5]);
await client.SendAsync(data);

// 等待接收数据处理完成
await tcs.Task;

// 验证接收到的数据
Assert.Equal(receivedBuffer.ToArray(), [1, 2, 3, 4, 5, 0x13, 0x10]);
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick (testing): Assertion order: expected and actual values should be swapped.

Swap the arguments so the expected value is first and the actual value is second: Assert.Equal([1, 2, 3, 4, 5, 0x13, 0x10], receivedBuffer.ToArray());. This improves test output clarity.


// 关闭连接
client.Close();
StopTcpServer(server);
}

private static TcpListener StartTcpServer(int port, Func<TcpClient, Task> handler)
{
var server = new TcpListener(IPAddress.Loopback, port);
Expand All @@ -280,6 +319,27 @@ private static async Task AcceptClientsAsync(TcpListener server, Func<TcpClient,
}
}

private static async Task MockDelimiterPackageAsync(TcpClient client)
{
using var stream = client.GetStream();
while (true)
{
var buffer = new byte[10240];
var len = await stream.ReadAsync(buffer);
if (len == 0)
{
break;
}

// 回写数据到客户端
var block = new Memory<byte>(buffer, 0, len);
await stream.WriteAsync(block, CancellationToken.None);

// 模拟拆包发送第二段数据
await stream.WriteAsync(new byte[] { 0x13, 0x10, 0x5, 0x6 }, CancellationToken.None);
}
}

private static async Task MockSplitPackageAsync(TcpClient client)
{
using var stream = client.GetStream();
Expand Down