--- 
id: customfixedheaderdatahandlingadapter
title: 模板解析“固定包头”数据适配器
---

### 定义

命名空间：TouchSocket.Core <br/>
程序集：[TouchSocket.Core.dll](https://www.nuget.org/packages/TouchSocket.Core)

## 一、说明

和用户自定义适配器相比，使用模板解析将会更加简单流程。例如在上节所说的数据格式，前三个字节是**固定长度**的，为3，而后续长度则由第一个字节计算可得，所以我们把类似这样的数据格式，叫做“**固定包头**”数据，那么，他就可以使用固定包头数据解析模板。

例如：假设如下数据格式。

- 第1个字节表示整个数据长度（包括数据类型和指令类型）
- 第2字节表示数据类型。
- 第3字节表示指令类型。
- 后续字节表示其他数据。

<img src={require('@site/static/img/docs/datahandleadapter-1.png').default}/>

## 二、特点

1. 可以自由适配**99%**的数据协议（例如：`modbus`，电力控制协议等）。
2. 可以随意定制数据协议。
3. 可以与**任意语言、框架**对接数据。


## 三、创建适配器

### 3.1 观察数据

一般来说，绝大多数数据协议都是**固定包头长度**的，例如：modbus协议，或者文本即将解析的数据格式。他们都是经典的固定包头格式，具有Header+Body的明显分割点。但有时候，也有一些数据有好几段，例如：具有Crc校验的数据，也就是Header+Body+Crc的格式，这时候，我们可以把Body+Crc看做一段数据，然后从Header解析BodyLength以后，加上Crc的长度。最后会在OnParsingBody时，将Body和Crc一起投递，届时做好数据分割即可。

所以，学会观察数据，是使用模板解析的前提。

### 3.2 创建适配器

声明一个类，实现`IFixedHeaderRequestInfo`接口，此对象即为存储数据的实体类，可在此类中声明一些属性，以备使用。

其中，`BodyLength`属性是接口必须的，用于标识后续数据长度。所以该值应该在`OnParsingHeader`时得到有效赋值。

```csharp showLineNumbers 
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。

```csharp showLineNumbers
public class MyFixedHeaderCustomDataHandlingAdapter : CustomFixedHeaderDataHandlingAdapter<MyFixedHeaderRequestInfo>
{
    /// <summary>
    /// 接口实现，指示固定包头长度
    /// </summary>
    public override int HeaderLength => 3;

    /// <summary>
    /// 获取新实例
    /// </summary>
    /// <returns></returns>
    protected override MyFixedHeaderRequestInfo GetInstance()
    {
        return new MyFixedHeaderRequestInfo();
    }
}
```

## 四、使用

```csharp {7,30}
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;
}
```

:::tip 提示

使用自定义适配器的数据，只能通过`IRequestInfo`来抛出，所以如果有传递其他数据的需求，可以多声明一些属性来完成。

:::  

:::tip 提示

上述创建的适配器客户端与服务器均适用。

:::  

## 五、适配器纠错

该适配器当收到半包数据时，会自动缓存半包数据，然后等后续数据收到以后执行拼接操作。

但有的时候，可能由于其他原因导致后续数据错乱，这时候就需要纠错。详情可看[适配器纠错](./adaptererrorcorrection.mdx)



[本文示例Demo](https://gitee.com/RRQM_Home/TouchSocket/tree/master/examples/Adapter/CustomFixedHeaderConsoleApp)
