介绍及使用
定义
命名空间:TouchSocket.Core
程序集:TouchSocket.Core.dll
一、说明
数据处理适配器,是TouchSocket的核心组件,其作用就是解析数据。
而解析可以分为两种,第一种是解决数据的黏、分包问题,第二种是将收到的数据解析为数据对象。
二、为什么需要适配器
适配器的作用就是解析数据,在TouchSocket系中,数据可分为流式单线程和非流式多线程两种。其中流式单线程以Tcp
,Namedpipe
、SerialPort
等为代表。非流式多线程则以Udp
为代表。接下来,我们会对其进行详细介绍。
2.1 流式单线程
顾名思义,流式单线程的数据,就是只有一个线程访问的流式数据,它像水流一样,在传输时滔滔不绝。其特点是:
- 数据包是有序的。
- 数据包是偶发性粘连的,即数据包之间没有分界,且粘 连的因素不确定。
- 在读取时,可能会读到多个数据包,或者多个半包。
那么如何解决粘包、半包问题呢?
实际上很好理解。既然数据是有序流式的,那么标识和顺位就是解决问题的关键。一般来说,通讯协议可分为固定包头
、不固定包头
、固定长度
和字符区间
四种。下面我们将以通俗易懂的方式来讲解。
假如,我们把传输的过程比作是一条行进中的队伍。把一个字节,比作队伍中的一个人。从宏观的角度来说,我们很难分辨队伍中有多少个小队,每个小队具体是做什么工作的。
但是我们可以在队伍中,每确定一个小队,就把小队第一个人任命为队长,并告诉他这支小队总共多少人。然后当队伍行进到终点时,只需要先问队长,小队有多少人,然后将这支小队归队。然后再问下一个队长,以此往复。鉴于队伍的连续性和有序性,只要队长不记错小队人数,那么我们必然能识别每个小队长,也能从他口中得知每个小队人数。
那么,这就是固定包头
的思路。不过很多时候,都不是一个字节标识长度,甚至固定的字节中还会有其他数据,但这并不影响固定包头的识别。
或者,我们并不知到队长在哪里,只能通过某种约定逐个询问,直到先找到队长,然后询问队长队伍中有多少人。
那么,这就是不固定包头
的思路。
或者,我们可以在队伍出发前,就和接收单位约定好,每个小队就是10个人,那么我们就可以在队伍到达终点时,每10个人归为一个小队。这样也能识别小队,也能知道小队人数。
那么,这就是固定长度
的思路。
或者,我们可以在队伍出发前,就做好标识,比如,遇到戴红帽子的,即视为队长,直到遇到下一个红帽子,这中间的所有人都归为一队。或者遇到蓝帽子的,即视为小队最后的那个人,即蓝帽子之前的人都归为一队。亦或者,一个小队,必须由红帽子开头,蓝帽子结尾。那么无论那种,我们很好的分辨出小队。
那么,这就是字符区间
的思路。
一般来说,99.5%的通讯协议,都是以上四种思路,或者其混合体。只要我们了解这些思路,那么就能很好的解决粘包、半包问题,而适配器的时候,则会大大简化这个操作。
2.2 非流式多线程
非流式多线程的数据,就是多个线程同时访问的数据。其特点是:
- 数据包是独立的。
- 数据包是无序的。
对于非流式多线程的数据,它一般不需要解决粘、分包问题。所以适配器的使用仅仅是将接收的数据包,按照IRequestInfo
的格式进行封装投递。
三、设计架构
3.1 工作逻辑
3.2 数据逻辑
TouchSocket的适配器,在初始阶段(原始Tcp),会收到一个ByteBlock数据,然后经过适配器处理以后,可选择两个参数(ByteBlock
和IRequestInfo
)的任意组合投递数据。
例如:FixedHeaderPackageAdapter,仅投递ByteBlock数据,届时IRequestInfo将为null。而如果是继承的CustomDataHandlingAdapter,则仅投递IRequestInfo,ByteBlock将为null。
3.3 设计解释
大家有时候可能会迷惑,为什么TouchSocket要设计两个参数投递,而不像其他的那样的,在会话里面,把适配器直接泛型规范了,直接抛出对应的类型。这是因为泛型约束太大,不够灵活。例如:
- 第一,不能随时切换适配器,例如适配WebSocket,在握手阶段,要解析http数据,所以,此时应该选择http数据处理适配,而完成握手以后,就要解析ws数据格式了,所以此时应该切换适配器为ws数据处理适配器。
- 第二,两个参数能提高性能。例如HTTP数据处理适配器,在高性能工作模式下,由
IRequestInfo
投递请求头,由ByteBlock
投递Body,这样Body是从内存池获得,就不存在内存消耗了。
四、Tcp使用
在Tcp系中使用数据处理适配器是非常简单的一个过程。而且为了不同场 景,TouchSocket支持多种方式的适配器使用。服务器和客户端使用一致
4.1 在Config配置中使用
在Config配置使用时,相当于初始化赋值。比较单一,但是很可靠。
var config = new TouchSocketConfig();
config.SetTcpDataHandlingAdapter(() => new NormalDataHandlingAdapter());
4.2 直接设置适配器
直接设置适配器,可以在任意时刻进行。
client.SetDataHandlingAdapter(new NormalDataHandlingAdapter());
五、Udp使用插件
Udp使用的插件,只能从Config配置。
【UdpSession】
m_udpSession.Setup(new TouchSocketConfig()
.SetBindIPHost(new IPHost(this.textBox2.Text))
.SetUdpDataHandlingAdapter(()=>
{
return new NormalUdpDataHandlingAdapter();
})
.ConfigureContainer(a =>
{
}));
m_udpSession.Start();
同一个适配器实例,只可被赋值一次,不然会异常。
六、限制使用
限制使用的场景应用于自定义封装。例如:自己封装一个服务器,然后适配 器仅特定使用,不允许外部随意赋值,那么可以如下实现:
public class MySessionClient : SessionClient
{
/// <summary>
/// <inheritdoc/>
/// </summary>
public override sealed bool CanSetDataHandlingAdapter => false;//不允许随意赋值
private void InternalSetAdapter(DataHandlingAdapter adapter)
{
this.SetAdapter(adapter);//仅继承内部赋值
}
}
七、缓存超时(仅Tcp适配器)
当适配器在工作时,如果一个数据包在设置的周期内(默认1000ms)没有完成接收,则会清空所有缓存数据,然后重新接收。 这种设计是为了应对,当接收数据时,如果发送方发送了异常数据(也有可能在移动网时,由运营商发送的无效包)而导致整个接收队列数据无效的问题。 现在加入缓存超时设置,则如果发送上述情况,也会在一个周期后,快速恢复接收。
相关属性设置:
- CacheTimeoutEnable:是否启用缓存超时。
- CacheTimeout:缓存超时时间
- UpdateCacheTimeWhenRev:是否每次接收就更新缓存时间。默认true,意味着只要有数据接收,则缓存永远不会过期。当为false时,则每个缓存包,必须在设置的周期内完成接收。