适配器纠错
定义
命名空间:TouchSocket.Core
程序集:TouchSocket.Core.dll
一、说明
适配器纠错功能,用于对接收的数据进行纠错。
二、纠错流式单线程数据
我们在适配器介绍中讲过,流式单线程数据的解析,标识和顺位就是解决问题的关键,这也就意味着一般来说,流式单线程数据是不允许有其他数据的。不然数据解析将会发生灾难性故障。
但是有时候,往往环境不是我们所控制的。在数据实际传输时,有极小的情况确实会发生其他数据包混入的情况。
此时,我们就可以使用适配器纠错功能,来解决这种问题。
2.1 纠错原理
对于纠错,我们的目的就是要重新定位到正确数据的起始位置,让数据能够重新解析。那么对于重新定位,我们有以下策略:
- 按照算法找到正确的起始位置。这要求数据格式本身就具有验证性。
- 即时重置缓存。
- 断开连接。
下列,我们将以Tcp数据为例,讲解如何使用适配器纠错功能。
2.2 纠错示例
我们假设一个常见的TLV数据,其格式为:
|Tag|Length|Value|
其中,Tag为4字节,Length为4字节,Value为可变长度的数据。
要解析该数据,我们非常容易的会想到使用模版解析固定包头适配器来解决问题。
所以代码大概如下:
public class MyFixedHeaderCustomDataHandlingAdapter : CustomFixedHeaderDataHandlingAdapter<MyFixedHeaderRequestInfo>
{
/// <summary>
/// 接口实现,指示固定包头长度
/// </summary>
public override int HeaderLength => 8;
/// <summary>
/// 获取新实例
/// </summary>
/// <returns></returns>
protected override MyFixedHeaderRequestInfo GetInstance()
{
return new MyFixedHeaderRequestInfo();
}
}
public class MyFixedHeaderRequestInfo : IFixedHeaderRequestInfo
{
public int Tag { get; set; }
public int Length => this.m_bodyLength;
public byte[] Value { get;private set; }
private int m_bodyLength;
int IFixedHeaderRequestInfo.BodyLength => this.m_bodyLength;
bool IFixedHeaderRequestInfo.OnParsingBody(byte[] body)
{
if (body.Length!=this.m_bodyLength)
{
return false;
}
this.Value = body;
return true;
}
bool IFixedHeaderRequestInfo.OnParsingHeader(byte[] header)
{
if (header.Length!=8)
{
return false;
}
this.Tag = TouchSocketBitConverter.BigEndian.ToInt32(header,0);
this.m_bodyLength = TouchSocketBitConverter.BigEndian.ToInt32(header,4);
return true;
}
}
此时,如果我们的数据是完全符合我们定义的协议的,那么我们的算法就是100%可靠的。
但是,如果我们的数据不符合我们的协议呢?例如:在Tag、Length、Value之间多出n个字节,那么我们的算法就无法解析了。
此时,我们需要对数据进行处理,进行纠错。
2.3 自我纠错
首先,我们来看能不能自己纠错。自己纠错,必须是数据格式自己能识别错误,并且能自己纠错。在此案例中,明显是不适用的,因为简单的TLV数据是不具备自我纠错能力的。
那么什么样的数据可以自我纠错呢?
例如:|Begin|Tag|TagCrc|Length|LengthCrc|Value|ValueCrc|End|
其中假设:Begin、End是固定的“**和##”,Tag、Length均为4字节,TagCrc、LengthCrc、ValueCrc是对应数据的CRC校验。
那么对于此类数据,可能具有一定纠错能力。
原因是,当在解析数据时,如果我们发现该数据并不是以“**”开头,则会认为该数据是错误的。可以在OnParsingHeader
中返回false
,表示数据错误。然后适配器会向后递推1个字节,再次尝试解析。Tag、Length、Value也可以使用crc校验来决定返回true
或false
。
聪明的小伙伴应该发现了,这种机制也只能解决部分问题。例如:当冗余数据出现在Tag、Length、Value数据间隔时,可能是有用的。一旦冗余数据在Tag、Length、Value之中时,我们仍然无法解析。所以,这种机制可能能解决部分问题。
但是,这仍然是有意义的,因为纠错的目的不仅仅是纠错还原本次数据,更主要的是能解析下次正确的数据。
2.4 即时重置缓存
即时重置缓存,是指当数据不完整时,立即重置缓存。这也就意味着,当数据不完整时,我们会立即放弃本次数据的解析。直接进行下一次数据的解析。
该操作对于用户自定义解析是容易的。您只需要调用this.Reset()
即可。
但是对于模版解析,可能需要传递一些委托来完成重置工作。
例如上述代码,我们可以这样写:
在MyFixedHeaderRequestInfo
的构造函数中,将MyFixedHeaderCustomDataHandlingAdapter
的Reset
函数通过委托传递给MyFixedHeaderRequestInfo
。
public class MyFixedHeaderCustomDataHandlingAdapter : CustomFixedHeaderDataHandlingAdapter<MyFixedHeaderRequestInfo>
{
/// <summary>
/// 接口实现,指示固定包头长度
/// </summary>
public override int HeaderLength => 8;
/// <summary>
/// 获取新实例
/// </summary>
/// <returns></returns>
protected override MyFixedHeaderRequestInfo GetInstance()
{
return new MyFixedHeaderRequestInfo(this.Reset);
}
}
public class MyFixedHeaderRequestInfo : IFixedHeaderRequestInfo
{
public MyFixedHeaderRequestInfo(Action action)
{
this.m_actionForReset = action;
}
public int Tag { get; set; }
public int Length => this.m_bodyLength;
public byte[] Value { get;private set; }
private int m_bodyLength;
private readonly Action m_actionForReset;
int IFixedHeaderRequestInfo.BodyLength => this.m_bodyLength;
bool IFixedHeaderRequestInfo.OnParsingBody(byte[] body)
{
if (body.Length!=this.m_bodyLength)
{
this.m_actionForReset.Invoke();
return false;
}
this.Value = body;
return true;
}
bool IFixedHeaderRequestInfo.OnParsingHeader(byte[] header)
{
if (header.Length!=8)
{
this.m_actionForReset.Invoke();
return false;
}
this.Tag = TouchSocketBitConverter.BigEndian.ToInt32(header,0);
this.m_bodyLength = TouchSocketBitConverter.BigEndian.ToInt32(header,4);
return true;
}
}
亦或者,使用适配器自带的缓存超时来处理。
例如下列设置:
启用了缓存超时,和超时更新。
这也就意味着,当我们收到一个错误数据时,只要等待1秒以后再重新发送,那么上个错误的缓存将不再有效,也就是此次数据会被正确解析。
public class MyFixedHeaderCustomDataHandlingAdapter : CustomFixedHeaderDataHandlingAdapter<MyFixedHeaderRequestInfo>
{
public MyFixedHeaderCustomDataHandlingAdapter()
{
this.CacheTimeoutEnable = true;
this.CacheTimeout=TimeSpan.FromSeconds(1);
this.UpdateCacheTimeWhenRev = true;
}
/// <summary>
/// 接口实现,指示固定包头长度
/// </summary>
public override int HeaderLength => 8;
/// <summary>
/// 获取新实例
/// </summary>
/// <returns></returns>
protected override MyFixedHeaderRequestInfo GetInstance()
{
return new MyFixedHeaderRequestInfo(this.Reset);
}
}
使用此配置,要注意发送数据的频次,如果是数秒甚至更大时间发送一次数据,那么这将是 有用的。
2.5 断开连接
当数据有异常时,直接断开连接,也不失为一种处理方式。同样,该操作对于用户自定义解析是容易的。您只需要调用Owner
的相关操作即可。例如:
可以声明一个方法。用于关闭连接。
private void Close()
{
if (this.Owner is ICloseObject client)
{
client.Close("适配器关闭");
}
}
同理,如果是模版解析,依然需要使用委托的方法,将Close()
函数传递到IRequestInfo
中。此处就不再赘述。