模板解析“非固定包头”数据适配器
定义
命名空间:TouchSocket.Core
程序集:TouchSocket.Core.dll
一、说明
有时候,我们需要解析的数据的包头是不定的,例如:HTTP数据格式,其数据头和数据体由“\r\n”隔开,而数据头又因为请求者的请求信息的不同,头部数据量不固定,而数据体的长度,也是由数据头的ContentLength的值显式指定的,所以,可以考虑使用CustomUnfixedHeaderDataHandlingAdapter解析。
二、特点
- 可以自由适配所有的数据协议。
- 可以随意定制数据协议。
- 可以与任意语言、框架对接数据。
三、使用
客户端与服务器均适用。下列以服务器为例。
步骤
- 声明新建类,实现IFixedHeaderRequestInfo接口,此对象即为存储数据的实体类,可在此类中声明一些属性,以备使用。
- 声明新建类,继承CustomUnfixedHeaderDataHandlingAdapter,并且以步骤1声明的类作为泛型。并实现对应抽象方法。
- TouchSocketConfig配置中设置。
- 通过Received(事件、方法、插件)中的RequestInfo对象,强转为步骤1声明的类型,然后读取其属性值,以备使用。
【MyUnfixedHeaderRequestInfo】
首先,新建MyFixedHeaderRequestInfo类,然后实现IUnfixedHeaderRequestInfo用户自定义固定包头接口。
然后在OnParsingHeader函数执行结束时,对实现的BodyLength
属性作出赋值,以此来决定,后续还应该接收多少数据作为Body 。
public class MyUnfixedHeaderRequestInfo : IUnfixedHeaderRequestInfo
{
/// <summary>
/// 接口实现,标识数据长度
/// </summary>
public int BodyLength { get; private set; }
/// <summary>
/// 自定义属性,标识数据类型
/// </summary>
public byte DataType { get; set; }
/// <summary>
/// 自定义属性,标识指令类型
/// </summary>
public byte OrderType { get; set; }
/// <summary>
/// 自定义属性,标识实际数据
/// </summary>
public byte[] Body { get; set; }
public int HeaderLength { get; private set; }
public bool OnParsingBody(ReadOnlySpan<byte> body)
{
if (body.Length == this.BodyLength)
{
this.Body = body.ToArray();
return true;
}
return false;
}
public bool OnParsingHeader<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlock
{
//在使用不固定包头解析时
//【首先】需要先解析包头
if (byteBlock.CanReadLength < 3)
{
//即直接缓存
return false;
}
//先保存一下初始游标,如果解析时还需要缓存,可能需要回退游标
var position = byteBlock.Position;
//【然后】ReadToSpan会递增游标,所以不需要再递增游标
var header = byteBlock.ReadToSpan(3);
//如果使用Span自行裁剪的话,就需要手动递增游标
//var header=byteBlock.Span.Slice(position,3);
//byteBlock.Position += 3;
//【然后】解析包头,和BodyLength
//在该示例中,第一个字节表示后续的所有数据长度,但是header设置的是3,所以后续还应当接收length-2个长度。
this.BodyLength = header[0] - 2;
this.DataType = header[1];
this.OrderType = header[2];
//【最后】对HeaderLength做有效赋值
this.HeaderLength = 3;
return true;
}
}
新建MyCustomUnfixedHeaderDataHandlingAdapter继承CustomUnfixedHeaderDataHandlingAdapter。
public class MyUnfixedHeaderCustomDataHandlingAdapter : CustomUnfixedHeaderDataHandlingAdapter<MyUnfixedHeaderRequestInfo>
{
protected override MyUnfixedHeaderRequestInfo GetInstance()
{
return new MyUnfixedHeaderRequestInfo();
}
}
【接收】
private static async Task Main(string[] args)
{
var service = await CreateService();
var client = await CreateClient();
ConsoleLogger.Default.Info("按任意键发送10次");
while (true)
{
Console.ReadKey();
for (var i = 0; i < 10; i++)
{
var myRequestInfo = new MyUnfixedHeaderRequestInfo()
{
Body = Encoding.UTF8.GetBytes("hello"),
DataType = (byte)i,
OrderType = (byte)i
};
//构建发送数据
using (var byteBlock = new ByteBlock(1024))
{
byteBlock.WriteByte((byte)(myRequestInfo.Body.Length + 2));//先写长度,因为该长度还包含数据类型和指令类型,所以+2
byteBlock.WriteByte((byte)myRequestInfo.DataType);//然后数据类型
byteBlock.WriteByte((byte)myRequestInfo.OrderType);//然后指令类型
byteBlock.Write(myRequestInfo.Body);//再写数据
await client.SendAsync(byteBlock.Memory);
}
}
}
}
private static async Task<TcpClient> CreateClient()
{
var client = new TcpClient();
//载入配置
await client.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:7789")
.SetTcpDataHandlingAdapter(() => new MyUnfixedHeaderCustomDataHandlingAdapter())
.ConfigureContainer(a =>
{
a.AddConsoleLogger();//添加一个日志注入
}));
await client.ConnectAsync();//调用连接,当连接不成功时,会抛出异常。
client.Logger.Info("客户端成功连接");
return client;
}
private static async Task<TcpService> CreateService()
{
var service = new TcpService();
service.Received = (client, e) =>
{
//从客户端收到信息
if (e.RequestInfo is MyUnfixedHeaderRequestInfo myRequest)
{
client.Logger.Info($"已从{client.Id}接收到:DataType={myRequest.DataType},OrderType={myRequest.OrderType},消息={Encoding.UTF8.GetString(myRequest.Body)}");
}
return Task.CompletedTask;
};
await service.SetupAsync(new TouchSocketConfig()//载入配置
.SetListenIPHosts("tcp://127.0.0.1:7789", 7790)//同时监听两个地址
.SetTcpDataHandlingAdapter(() => new MyUnfixedHeaderCustomDataHandlingAdapter())
.ConfigureContainer(a =>
{
a.AddConsoleLogger();//添加一个控制台日志注入(注意:在maui中控制台日志不可用)
})
.ConfigurePlugins(a =>
{
//a.Add();//此处可以添加插件
}));
await service.StartAsync();//启动
service.Logger.Info("服务器已启动");
return service;
}
提示
上述创建的适配器客户端与服务器均适用。
四、适配器纠错
该适配器当收到半包数据时,会自动缓存半包数据,然后等后续数据收到以后执行拼接操作。
但有的时候,可能由于其他原因导致后续数据错乱,这时候就需要纠错。详情可看适配器纠错