Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8cf8794
chore: 更新解决方案增加扩展包
ArgoZhang Jun 20, 2025
c31b545
feat: 增加 ReceivedCallBack 回调方法
ArgoZhang Jun 20, 2025
7c39c48
test: 增加单元测试
ArgoZhang Jun 20, 2025
081236b
doc: 更新示例文档
ArgoZhang Jun 20, 2025
712e0dc
test: 增加 TouchSocket 单元测试
ArgoZhang Jun 20, 2025
d32f100
doc: 更新粘包分包文档
ArgoZhang Jun 20, 2025
4e78aba
feat: 增加 SendAsync 扩展方法
ArgoZhang Jun 20, 2025
52334b7
test: 增加单元测试
ArgoZhang Jun 20, 2025
a533852
Merge branch 'main' into feat-TouchSocket
ArgoZhang Jun 20, 2025
859b0cc
feat(tcp): 替换同步关闭方法为异步方法
RRQM Jun 20, 2025
82a93f4
refactor(tests): 使用异步方法关闭连接
RRQM Jun 20, 2025
517dc1f
doc: 更新数据处理器相关文档
ArgoZhang Jun 21, 2025
0f328a3
refactor: 增加 ConnectAsync 扩展方法
ArgoZhang Jun 21, 2025
336dd47
refactor: 更改关闭方法为同步方法
ArgoZhang Jun 21, 2025
93f68b6
test: 更新单元测试
ArgoZhang Jun 21, 2025
686e03b
Merge branch 'main' into feat-TouchSocket
ArgoZhang Jun 21, 2025
e23296f
doc: 增加数据适配器文档
ArgoZhang Jun 22, 2025
0f86b3c
doc: 增加新服务标记
ArgoZhang Jun 22, 2025
a2767ab
refactor: 更新 ITcpSocketFactory 接口定义
ArgoZhang Jun 22, 2025
60552bb
feat: 增加 GetOrCreate 扩展方法
ArgoZhang Jun 22, 2025
544fcd2
test: 更新单元测试
ArgoZhang Jun 22, 2025
d132af3
test: 增加单元测试
ArgoZhang Jun 22, 2025
66ab098
Merge branch 'main' into feat-TouchSocket
ArgoZhang Jun 23, 2025
0edee26
refactor: 更改 GetOrCreate 签名
ArgoZhang Jun 23, 2025
03bf822
test: 更新单元测试
ArgoZhang Jun 23, 2025
86a0333
refactor: 根据最新设计重构 TouchSocket 实现
ArgoZhang Jun 23, 2025
a97bc71
refactor: 整理项目文件
ArgoZhang Jun 23, 2025
7fc2eb1
Merge branch 'main' into feat-TouchSocket
ArgoZhang Jun 23, 2025
cc1a7ef
wip: 临时移除稍后再开分支合并到另外分支上
ArgoZhang Jun 23, 2025
1edbe34
chore: bump version 9.7.4-beta08
ArgoZhang Jun 23, 2025
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
7 changes: 7 additions & 0 deletions BootstrapBlazor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{9BAF50BE
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTest.Benchmarks", "tools\Benchmarks\UnitTest.Benchmarks.csproj", "{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.TouchSocket", "..\BootstrapBlazor.Extensions\src\extensions\BootstrapBlazor.TouchSocket\BootstrapBlazor.TouchSocket.csproj", "{8B1D923C-5B6E-6068-BDAE-2752F7FF2D11}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -112,6 +114,10 @@ Global
{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1}.Release|Any CPU.Build.0 = Release|Any CPU
{8B1D923C-5B6E-6068-BDAE-2752F7FF2D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B1D923C-5B6E-6068-BDAE-2752F7FF2D11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B1D923C-5B6E-6068-BDAE-2752F7FF2D11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B1D923C-5B6E-6068-BDAE-2752F7FF2D11}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -128,6 +134,7 @@ Global
{D8AEAFE7-10AF-4A5B-BC67-FE740A2CA1DF} = {7C1D79F1-87BC-42C1-BD5A-CDE4044AC1BD}
{C075C6C8-B9CB-4AC0-9BDF-B2002B4AB99C} = {EA765165-0542-41C8-93F2-85787FEDEDFF}
{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1} = {9BAF50BE-141D-4429-93A9-942F373D1F68}
{8B1D923C-5B6E-6068-BDAE-2752F7FF2D11} = {A2182155-43ED-44C1-BF6F-1B70EBD2DFFE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0DCB0756-34FA-4FD0-AE1D-D3F08B5B3A6B}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ private ITcpSocketFactory? TcpSocketFactory { get; set; }</Pre>
<li>通过 <code>ITcpSocketClient</code> 实例方法 <code>SendAsync</code> 发送协议数据</li>
<li>通过 <code>ITcpSocketClient</code> 实例方法 <code>Close</code> 关闭连接</li>
<li>通过 <code>ITcpSocketClient</code> 实例方法 <code>SetDataHandler</code> 方法设置数据处理器</li>
<li>通过 <code>ITcpSocketClient</code> 实例属性 <code>ReceivedCallBack</code> 方法设置接收数据处理器(注意:此回调未做任何数据处理为原始数据)</li>
</ul>

<p class="code-label">4. 数据处理器</p>
Expand All @@ -31,13 +32,13 @@ private ITcpSocketFactory? TcpSocketFactory { get; set; }</Pre>
<p>数据处理器设计初衷就是为了契合通讯协议大大简化我们开发逻辑,我们已通讯协议每次通讯电文均为 <b>4</b> 位定长举例说明,在实际的通讯过程中,我们接收到的通讯数据存在粘包或者分包的现象</p>

<ul class="ul-demo">
<li>粘包:比如我们期望收到 <b>1234</b> 四个字符,实际上我们接收到的是 <b>123412</b> 多出来的 <b>12</b> 其实是下一个通讯电文的内容,相邻两个通讯数据包的粘连称为粘包</li>
<li>分包:比如我们期望收到 <b>1234</b> 四个字符,实际上我们接收到的是 <b>12</b> 和 <b>34</b> 两个数据包,这种情况称为分包</li>
<li><b>粘包</b>:比如我们期望收到 <b>1234</b> 四个字符,实际上我们接收到的是 <b>123412</b> 多出来的 <b>12</b> 其实是下一个数据包的内容,我们需要截取前 4 位数据作为一个数据包才能正确处理数据,这种相邻两个通讯数据包的粘连称为<b>粘包</b></li>
<li><b>分包</b>:比如我们期望收到 <b>1234</b> 四个字符,实际上我们可能分两次接收到,分别是 <b>12</b> 和 <b>34</b>,我们需要将两个数据包拼接成一个才能正确的处理数据。这种情况称为<b>分包</b></li>
</ul>

<p>我们内置了 <code>IDataPackageHandler</code> 数据包处理接口,已经虚类 <code>DataPackageHandlerBase</code> 作为数据处理器基类已经内置了 <b>粘包</b> <b>分包</b> 的逻辑,继承此类后专注自己处理的业务即可</p>
<p>我们内置了一些常用的数据处理类 <code>IDataPackageHandler</code> 接口为数据包处理接口,虚类 <code>DataPackageHandlerBase</code> 作为数据处理器基类已经内置了 <b>粘包</b> <b>分包</b> 的逻辑,继承此类后专注自己处理的业务即可</p>

<p>此外我们还内置了 <code>FixLengthDataPackageHandler</code> <b>固定长度数据处理器</b> 使用方法如下:</p>
<p>使用方法如下:</p>

<Pre>[Inject]
[NotNull]
Expand All @@ -64,3 +65,10 @@ private async Task CreateClient()
var connected = await client.ConnectAsync("192.168.10.100", 6688);
}
</Pre>

<p>内置数据库处理器</p>

<ul class="ul-demo">
<li><code>FixLengthDataPackageHandler</code> <b>固定长度数据处理器</b> 即每个通讯包都是固定长度</li>
<li><code>DelimiterDataPackageHandler</code> <b>分隔符数据处理器</b> 即通讯包以特定一个或一组字节分割</li>
</ul>
33 changes: 33 additions & 0 deletions src/BootstrapBlazor/Extensions/ITcpSocketClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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.Text;

namespace BootstrapBlazor.Components;

/// <summary>
/// <see cref="ITcpSocketClient"/> 扩展方法类
/// </summary>
public static class ITcpSocketClientExtensions
{
/// <summary>
/// Sends the specified string content to the connected TCP socket client asynchronously.
/// </summary>
/// <remarks>This method converts the provided string content into a byte array using the specified
/// encoding (or UTF-8 by default) and sends it to the connected TCP socket client. Ensure the client is connected
/// before calling this method.</remarks>
/// <param name="client">The TCP socket client to which the content will be sent. Cannot be <see langword="null"/>.</param>
/// <param name="content">The string content to send. Cannot be <see langword="null"/> or empty.</param>
/// <param name="encoding">The character encoding to use for converting the string content to bytes. If <see langword="null"/>, UTF-8
/// encoding is used by default.</param>
/// <param name="token">A <see cref="CancellationToken"/> to observe while waiting for the operation to complete.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> that represents the asynchronous operation. The result is <see
/// langword="true"/> if the content was sent successfully; otherwise, <see langword="false"/>.</returns>
public static ValueTask<bool> SendAsync(this ITcpSocketClient client, string content, Encoding? encoding = null, CancellationToken token = default)
{
var buffer = encoding?.GetBytes(content) ?? Encoding.UTF8.GetBytes(content);
return client.SendAsync(buffer, token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ public abstract class DataPackageHandlerBase : IDataPackageHandler
private Memory<byte> _lastReceiveBuffer = Memory<byte>.Empty;

/// <summary>
/// 当接收数据处理完成后,回调该函数执行接收
/// Gets or sets the callback function to handle received data.
/// </summary>
/// <remarks>The callback function should be designed to handle the received data efficiently and
/// asynchronously. Ensure that the implementation does not block or perform long-running operations, as this may
/// impact performance.</remarks>
public Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack { get; set; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class DefaultTcpSocketClient : ITcpSocketClient

public int ReceiveBufferSize { get; set; } = 1024 * 10;

public Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack { get; set; }

public DefaultTcpSocketClient(string host, int port = 0)
{
LocalEndPoint = new IPEndPoint(GetIPAddress(host), port);
Expand Down Expand Up @@ -136,6 +138,11 @@ private async ValueTask ReceiveAsync()
{
buffer = buffer[..len];

if (ReceivedCallBack != null)
{
await ReceivedCallBack(buffer);
}

if (_dataPackageHandler != null)
{
await _dataPackageHandler.ReceiveAsync(buffer);
Expand Down
8 changes: 8 additions & 0 deletions src/BootstrapBlazor/Services/TcpSocket/ITcpSocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public interface ITcpSocketClient : IDisposable
/// specific local endpoint, this property may return <see langword="null"/>.</remarks>
IPEndPoint LocalEndPoint { get; }

/// <summary>
/// Gets or sets the callback function to handle received data.
/// </summary>
/// <remarks>The callback function should be designed to handle the received data efficiently and
/// asynchronously. Ensure that the implementation does not block or perform long-running operations, as this may
/// impact performance.</remarks>
Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack { get; set; }

/// <summary>
/// Configures the data handler to process incoming data packages.
/// </summary>
Expand Down
21 changes: 18 additions & 3 deletions test/UnitTest/Services/TcpSocketFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.Logging;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace UnitTest.Services;

Expand Down Expand Up @@ -94,8 +95,7 @@ public async Task SendAsync_Cancel()
var cst = new CancellationTokenSource();
cst.Cancel();

var data = new ReadOnlyMemory<byte>([1, 2, 3, 4, 5]);
var result = await client.SendAsync(data, cst.Token);
var result = await client.SendAsync("test", null, cst.Token);
Assert.False(result);

// 设置延时发送适配器
Expand All @@ -110,7 +110,7 @@ public async Task SendAsync_Cancel()
// 测试发送失败逻辑
_ = Task.Run(async () =>
{
sendResult = await client.SendAsync(data);
sendResult = await client.SendAsync("test", Encoding.UTF8);
tcs.SetResult();
});

Expand Down Expand Up @@ -143,12 +143,27 @@ public async Task ReceiveAsync_Error()
Assert.Equal(1024 * 20, client.ReceiveBufferSize);

client.SetDataHandler(new MockReceiveErrorHandler());

ReadOnlyMemory<byte> buffer = ReadOnlyMemory<byte>.Empty;
var tcs = new TaskCompletionSource();

// 增加接收回调方法
client.ReceivedCallBack = b =>
{
buffer = b;
tcs.SetResult();
return ValueTask.CompletedTask;
};

await client.ConnectAsync("localhost", port);

// 发送数据导致接收数据异常
var data = new ReadOnlyMemory<byte>([1, 2, 3, 4, 5]);
await client.SendAsync(data);

await tcs.Task;
Assert.Equal(buffer.ToArray(), [1, 2, 3, 4, 5]);

// 关闭连接
StopTcpServer(server);
}
Expand Down
150 changes: 150 additions & 0 deletions test/UnitTest/Services/TouchSocketTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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 Microsoft.Extensions.Logging;
using System.Net;
using System.Net.Sockets;

namespace UnitTest.Services;

public class TouchSocketTest
{
[Fact]
public async Task FixLengthDataPackageHandler_Ok()
{
var port = 8884;
var server = StartTcpServer(port, MockSplitPackageAsync);
var client = CreateClient();

// 测试 ConnectAsync 方法
var connect = await client.ConnectAsync("localhost", port);
Assert.True(connect);
Assert.True(client.IsConnected);

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

// 增加数据处理适配器
client.SetDataHandler(new FixLengthDataPackageHandler(7)
{
ReceivedCallBack = buffer =>
{
receivedBuffer = buffer;
tcs.SetResult();
return ValueTask.CompletedTask;
}
});

// 测试 SendAsync 方法
var data = new ReadOnlyMemory<byte>([1, 2, 3, 4, 5]);
var result = await client.SendAsync(data);
Assert.True(result);

await tcs.Task;
Assert.Equal(receivedBuffer.ToArray(), [1, 2, 3, 4, 5, 3, 4]);

// 模拟延时等待内部继续读取逻辑完成,测试内部 _receiveCancellationTokenSource 取消逻辑
await Task.Delay(10);

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

private static async Task MockSplitPackageAsync(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 ReadOnlyMemory<byte>(buffer, 0, len);
await stream.WriteAsync(block, CancellationToken.None);

// 模拟延时
await Task.Delay(50);

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

private static TcpListener StartTcpServer(int port, Func<TcpClient, Task> handler)
{
var server = new TcpListener(IPAddress.Loopback, port);
server.Start();
Task.Run(() => AcceptClientsAsync(server, handler));
return server;
}

private static async Task AcceptClientsAsync(TcpListener server, Func<TcpClient, Task> handler)
{
while (true)
{
var client = await server.AcceptTcpClientAsync();
_ = Task.Run(() => handler(client));
}
}

private static void StopTcpServer(TcpListener server)
{
server?.Stop();
}

private static ITcpSocketClient CreateClient()
{
var sc = new ServiceCollection();
sc.AddLogging(builder =>
{
builder.AddProvider(new MockLoggerProvider());
});
sc.AddBootstrapBlazorTcpSocketFactory();

// 增加 TouchSocket 服务
sc.AddBootstrapBlazorTouchSocketService();

Check failure on line 112 in test/UnitTest/Services/TouchSocketTest.cs

View workflow job for this annotation

GitHub Actions / run test

'ServiceCollection' does not contain a definition for 'AddBootstrapBlazorTouchSocketService' and no accessible extension method 'AddBootstrapBlazorTouchSocketService' accepting a first argument of type 'ServiceCollection' could be found (are you missing a using directive or an assembly reference?)

var provider = sc.BuildServiceProvider();
var factory = provider.GetRequiredService<ITcpSocketFactory>();
var client = factory.GetOrCreate("localhost", 0);
return client;
}

class MockLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new MockLogger();
}

public void Dispose()
{

}
}

class MockLogger : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return null;
}

public bool IsEnabled(LogLevel logLevel)
{
return true;
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{

}
}
}
1 change: 1 addition & 0 deletions test/UnitTest/UnitTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\BootstrapBlazor.Extensions\src\extensions\BootstrapBlazor.TouchSocket\BootstrapBlazor.TouchSocket.csproj" />
<ProjectReference Include="..\..\src\BootstrapBlazor\BootstrapBlazor.csproj" />
</ItemGroup>

Expand Down
Loading