模板解析“固定包头”数据适配器
定义
命名空间:TouchSocket.Core
程序集:TouchSocket.Core.dll
一、说明
和用户自定义适配器相比,使用模板解析将会更加简单流程。例如在上节所说的数据格式,前三个字节是固定长度的,为3,而后续长度则由第一个字节计算可得,所以我们把类似这样的数据格式,叫做“固定包头”数据,那么,他就可以使用固定包头数据解析模板。
例如:假设如下数据格式。
- 第1个字节表示整个数据长度(包括数据类型和指令类型)
- 第2字节表示数据类型。
- 第3字节表示指令类型。
- 后续字节表示其他数据。
二、特点
- 可以自由适配**99%**的数据协议(例如:
modbus
,电力控制协议等)。 - 可以随意定制数据协议。
- 可以与任意语言、框架对接数据。
三、创建适配器
3.1 观察数据
一般来说,绝大多数数据协议都是固定包头长度的,例如:modbus协议,或者文本即将解析的数据格式。他们都是经典的固定包头格式,具有Header+Body的明显分割点。但有时候,也有一些数据有好几段,例如:具有Crc校验的数据,也就是Header+Body+Crc的格式,这时候,我们可以把Body+Crc看做一段数据,然后从Header解析BodyLength以后,加上Crc的长度。最后会在OnParsingBody时,将Body和Crc一起投递,届时做好数据分割即可。
所以,学会观察数据,是使用模板解析的前提。
3.2 创建适配器
声明一个类,实现IFixedHeaderRequestInfo
接口,此对象即为存储数据的实体类,可在此类中声明一些属性,以备使用。
其中,BodyLength
属性是接口必须的,用于标识后续数据长度。所以该值应该在OnParsingHeader
时得到有效赋值。
public class MyFixedHeaderRequestInfo : IFixedHeaderRequestInfo
{
/// <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 bool OnParsingBody(ReadOnlySpan<byte> body)
{
if (body.Length == this.BodyLength)
{
this.Body = body.ToArray();
return true;
}
return false;
}
public bool OnParsingHeader(ReadOnlySpan<byte> header)
{
//在该示例中,第一个字节表示后续的所有数据长度,但是header设置的是3,所以后续还应当接收length-2个长度。
this.BodyLength = header[0] - 2;
this.DataType = header[1];
this.OrderType = header[2];
return true;
}
}
然后声明新建类,继承CustomFixedHeaderDataHandlingAdapter
,并且以步骤1声明的类作为泛型。并实现对应抽象方法。
新建MyFixedHeaderCustomDataHandlingAdapter继承CustomFixedHeaderDataHandlingAdapter,然后对HeaderLength
作出赋值,以此表明固定包头的长度是多少。
在本案例中,我们是以前三个字节作为固定包头长度,所以HeaderLength
的赋值是3。
public class MyFixedHeaderCustomDataHandlingAdapter : CustomFixedHeaderDataHandlingAdapter<MyFixedHeaderRequestInfo>
{
/// <summary>
/// 接口实现,指示固定包头长度
/// </summary>
public override int HeaderLength => 3;
/// <summary>
/// 获取新实例
/// </summary>
/// <returns></returns>
protected override MyFixedHeaderRequestInfo GetInstance()
{
return new MyFixedHeaderRequestInfo();
}
}
四、使用
private static async Task<TcpClient> CreateClient()
{
var client = new TcpClient();
//载入配置
await client.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:7789")
.SetTcpDataHandlingAdapter(() => new MyFixedHeaderCustomDataHandlingAdapter())
.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 MyFixedHeaderRequestInfo 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 MyFixedHeaderCustomDataHandlingAdapter())
.ConfigureContainer(a =>
{
a.AddConsoleLogger();//添加一个控制台日志注入(注意:在maui中控制台日志不可用)
})
.ConfigurePlugins(a =>
{
//a.Add();//此处可以添加插件
}));
await service.StartAsync();//启动
service.Logger.Info("服务器已启动");
return service;
}
使用自定义适配器的数据,只能通过IRequestInfo
来抛出,所以如果有传递其他数据的需求,可以多声明一些属性来完成。
上述创建的适配器客户端与服务器均适用。
五、适配器纠错
该适配器当收到半包数据时,会自动缓存半包数据,然后等后续数据收到以后执行拼接操作。
但有的时候,可能由于其他原因导致后续数据错乱,这时候就需要纠错。详情可看适配器纠错