System.IO.Pipelines 解决了以往接收 NetworkStream 算位置的困扰

微软官方博客在去年发布了一篇 System.IO.Pipelines: High performance IO in .NET,这个东西高不高效我是比较不出来,但是它着实解决了以往在接收 NetworkStream 时算位置的困扰,用它写出来的程序简洁清晰许多。

以往我们接收 NetworkStream 时,由于 Buffer 的大小通常是固定的,而串流数据的长度是不固定的,所以我们在解析的时候,每一种协定就有一套计算读取位置的逻辑,这种逻辑通常脑袋是记不住的,当协定久久异动一次的时候,我们就要开始担心原本的解析逻辑是不是又要改了,相当痛苦。

下面是一个简单的范例,假定 Server 与 Client 之间约定了一组协定,数据的处理逻辑以 “包” 为基本的处理单位,Server 会将数据一包一包地传给 Client,以换行符号 n 作为分隔符号,遇到换行符号时就代表一包数据结束,一包数据的长度则不固定,过去我们处理这类需求的程序大概就像下面这样,需要去考虑到一个 Buffer 会有 0 ~ n 个换行符号。

// 取得 NetworkStream
var stream = client.GetStream();

// 声明数据缓冲区
var line = new List();

while (true)
{
    // 设定 Buffer 长度
    var buffer = new byte[10];

    // 读取一个 Buffer 长度的数据
    var numBytesRead = stream.Read(buffer, 0, buffer.Length);

    int newlinePosition;
    int numBytesConsumed = 0;

    do
    {
        // 搜寻换行符号的位置
        newlinePosition = Array.IndexOf(buffer, (byte)'n', numBytesConsumed);

        if (newlinePosition >= 0)
        {
            // 将换行符号之间的数据放进缓冲区
            line.Add(buffer.Skip(numBytesConsumed).Take(newlinePosition - numBytesConsumed).ToArray());

            // 标记已经处理的数据长度
            numBytesConsumed = newlinePosition + 1;

            // 缓冲区内的数据已成一包,送给逻辑进程处理。
            ProcessData(line.SelectMany(x => x).ToArray());

            // 清空缓冲区
            line.Clear();
        }
        else
        {
            // 将剩余的数据放进缓冲区
            line.Add(buffer.Skip(numBytesConsumed).Take(numBytesRead - numBytesConsumed).ToArray());
        }
    }
    while (newlinePosition >= 0);
}

如果没有注解,过一阵子之后,这段程序就变天书了,需要通灵才能唤回过去记忆,写类似这种按照协定读取串流数据的程序是挺痛苦的,但是 System.IO.Pipelines 把这件事变简单了,我们来看一下要怎么使用它?

顾名思义,Pipeline 就是管道,我们可以这样想像,有一根管子,管子的一头不停地往里面塞东西,管子的另外一头,可以依照自己想要的长度,一次地把东西从管子里面拿出来,因此我们的程序会分成两段,一段是不停地往管子里面写数据,一段是依照我们想要的逻辑从管子中读数据。

使用 System.IO.Pipelines 我们从 new 一个 Pipe 开始,Pipe 里面会有一个 Writer 属性及 Reader 属性,Writer 就是往管子写数据,Reader 就是从管子读数据。

var pipe = new Pipe();
var writer = pipe.Writer;
var reader = pipe.Reader;

往管子写数据

// 取得 NetworkStream
var stream = client.GetStream();

while (true)
{
    // 设定 Buffer 长度
    var buffer = new byte[10];

    // 读取一个 Buffer 长度的数据
    var numBytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);

    if (numBytesRead == 0) continue;

    // 将数据写进 Pipe
    var flushResult = await writer.WriteAsync(new ReadOnlyMemory(buffer.Take(numBytesRead).ToArray()));

    if (flushResult.IsCompleted) break;
}

从 NetworkStream 读取数据的部分保持不变,然后调用 PipeWriter.WriteAsync() 方法将数据写入管子内。

从管子读数据

while (true)
{
    // 读取管子内目前的数据状况
    var result = await reader.ReadAsync();

    var buffer = result.Buffer;

    do
    {
        // 搜寻换行符号的位置
        var position = buffer.PositionOf((byte)'n');

        if (position == null) break;

        // 将管子内的换行符号前的数据,送给逻辑进程处理。
        ProcessData(buffer.Slice(0, position.Value));
        
        // 从目前搜寻到的换行符号的下一个位置,再搜寻换行符号。
        var next = buffer.GetPosition(1, position.Value);

        buffer = buffer.Slice(next);
    }
    while (true);
    
    // 标记管子内有多少数据已经被读取并处理,主要是释放管子的空间,让 Writer 可以重复利用。
    // 有调用 ReadAsync() 就一定要调用 AdvanceTo(),即使没有处理到任何数据也是一样。
    reader.AdvanceTo(buffer.Start, buffer.End);
}

这边就可以撰写我们解析数据的逻辑,可以看到我们不用再去算位置了,当符合规则时,直接把整段数据取出来使用,大大降低了程序的复杂性,而且 Pipe 是 Thread-Safe 的,Writer 跟 Reader 可以分开两个线程执行,不用担心同步的问题。

眉角:ReadResult.Buffer 的解读方式

如果我们的解析逻辑不是那么的单纯,难免需要对 ReadResult.Buffer 进行裁切,它的类型是 ReadOnlySequence,里面有一个 Slice() 方法可以让我们来对 Buffer 做裁切,而且 Slice() 方法提供 9 个重载,已经足够我们使用了,而我常用到的是这两个方法:

public ReadOnlySequence Slice(SequencePosition start, SequencePosition end);
public ReadOnlySequence Slice(SequencePosition start, int length);

Buffer 有两个属性 StartEnd 代表 Buffer 的起迄位置,Start 跟 End 的类型是 SequencePosition,这个东西跟以往我们熟悉的数组索引不一样,一开始没有弄清楚,结果裁切出来的数据不是我要的。

我们熟悉的数组及其索引是长这样的:

而 SequencePosition 这个东西要这样看:

如果上图是一个 ReadResult.Buffer,那么它的 Start 就是 0,End 就是 8,也因为这样的特性,所以有一些方法的参数或回传值是 SequencePosition 类型的,就要特别注意一下,像是 PositionOf() 扩充方法,以上图为例,如果我调用 PositionOf((byte)'d') 得到的 SequencePosition 值会是 3。

还有像是 PipeReader.AdvanceTo() 方法,它其实是帮我们在 Buffer 中,将标记已处理数据的位置移到参数所指定的位置,以上图为例,假设 SequencePosition 的值是 3,丢进 AdvanceTo() 方法之后,那么下一次 PipeReader 读取的结果 d 就会是第一个元素。

PositionOf() 扩充方法有用到 C# 7.2 才新增的语言特性,所以要调整项目的建置语言版本至少到 C# 7.2。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
System.IO.Pipelines是Microsoft开发的用于处理高性能I/O操作的库。它提供了一种简化的方式来读写数据流,并且在处理大量数据时具有出色的性能。 串口通信是一种用于在计机和外部设备之间传输数据的通信方式。通过串口,计机可以与各种外部设备进行通信,如传感器、机器人、打印机等。 System.IO.Pipelines库可以在串口通信中发挥重要作用。使用该库,我们可以通过创建一个Pipeline对象,来轻松处理从串口接收到的数据,并对接收和发送的数据进行高效的处理。 首先,我们可以使用System.IO.Pipelines.PipelineReader从串口中读取数据。通过调用ReadAsync方法,我们可以异步地读取串口中的数据,并将其放入到一个缓冲区中。然后,我们可以通过提供的Read方法来处理这些数据,比如解析、处理或存储。 其次,我们可以使用System.IO.Pipelines.PipelineWriter向串口发送数据。通过调用WriteAsync方法,我们可以异步地将数据写入到串口中。该方法会返回一个可用于链式编程的WritableBuffer对象,我们可以使用其提供的方法来构建数据流,并最终将数据发送到串口。 最后,System.IO.Pipelines库还提供了一些高级功能,以帮助我们更好地处理串口通信。例如,我们可以使用PipeScheduler来调度读写操作,以充分利用系统资源。我们还可以使用MemoryPoolOptions来自定义内存池的大小和数量,以适应不同的数据量和性能要求。 总之,System.IO.Pipelines是一个非常有用且高性能的库,可用于处理串口通信。它提供了简单易用的API,并具有出色的性能和灵活性,可以帮助我们更好地处理和管理串口数据的读写操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值