实现异步“从流中读取所有当前可用的数据”操作

时间:2010-12-21 23:16:45

标签: c# asynchronous console stream filestream

3 个答案:

答案 0 :(得分:2)

理论上,我同意杰森;如果一大块数据可以被缓冲区整除,那么你的实现比一个逻辑漏洞有更大的问题。我看到的最大问题是你的读者必须对文件类型有足够的了解才能知道如何将数据分成你的订阅者知道如何处理的“块”。

Streams没有关于他们接收或发送内容的固有知识;只有它们传输数据的机制。 NetworkStream可能正在发送HTML或ZIP文件; FileStream可能正在读取文本文件或MP3。它是具有这种知识的读者(XmlReader,TextReader,Image.FromStream()等)。因此,您的异步读取器必须至少知道有关数据的内容,但是不要将这些知识硬编码是有用的。

为了使用“流”数据,增量发送必须是单独有用的;你必须充分了解你所知道的,你所获得的是一个可以单独处理的“块”。我的建议是以封装的方式向您的异步阅读器提供该信息,方法是让您的订阅者告诉您,或者通过提供一些特定格式的“chunkifier”与听众分开(因为这个读者正在监听控制台输出,以及所有听众应该以同样的方式对待它,这第二个计划可能会更好。)

逻辑实施:

public class MyStreamManager {
    public delegate bool ValidChunkTester(StringBuilder builder);

    private readonly List<ValidChunkTester> validators = new List<ValidChunkTester>();
    public event ValidChunkTester IsValidChunk
    { add{validators.Add(value);} remove {validators.Remove(value);}}

    public event EventHandler<ConsoleOutputReadEventArgs> StandardOutputRead;


    public void StartSendingEvents();
    public void StopSendingEvents();
}

...

private void ReadHappened(IAsyncResult asyncResult)
{
    var bytesRead = this.StandardOutput.BaseStream.EndRead(asyncResult);
    if (bytesRead == 0) {
        this.OnAutomationStopped();
        return;
    }

    var input = this.StandardOutput.CurrentEncoding.GetString(
        this.buffer, 0, bytesRead);
    this.inputAccumulator.Append(input);

    if (validators.Any() && StandardOutputRead !-= null 
            && validators.Aggregate(true, (valid, validator)=>valid && validator(inputAccumulator))) {
        this.OnInputRead(); // send when all listeners can work with the buffer contents
    }

    this.BeginReadAsync(); // continue "looping" with BeginRead
}

...

此模型要求订阅者不要修改StringBuilder;如果你愿意,你可以为他们提供一些不可改变的东西。示例监听器可能是:

public bool IsACompleteLine(StringBuilder builder)
{
    return builder.Contains(Environment.NewLine);
}

或:

public bool Contains256Bytes(StringBuilder builder)
{
    return builder.Length >= 256;
}

...你明白了。确定要释放给侦听器的当前缓冲区的价值的事件在概念上与侦听器本身是分开的,但不一定具体,因此它将支持单个输出特定测试或多个基于侦听器的测试。

答案 1 :(得分:1)

如果以您描述的方式从FileStream读取,则将读取基础文件的全部内容。因此,您将只有一个“数据块”,您将在微小的咬合中读入StringBuilder(有点低效)。您的实现中没有任何内容可以将数据分解为更小的“块”,因为读取将继续填充缓冲区,直到文件耗尽为止。在这个抽象级别,只有客户端知道这些块应该是多大,所以你必须将数据交给它们才能被解码成块。这违背了缓冲区的最初目的。

如果你有其他形式的流以突发形式传递数据(例如,控制台输出或通信数据包),那么你将获得数据突发,但你仍然无法保证读取结束时的缓冲区少于缓冲区一些数据意味着您已到达数据包的末尾,只是传输中有暂停。通常在这些情况下,您需要缓冲数据并对其进行处理(即具有数据格式的知识),以确定何时接收到完整的块/数据包。在这种情况下,您将始终在缓冲区中等待“未完成的块”,直到收到更多数据来终止块或启动新块,并将其“推出”缓冲区。这可能是通信中的问题,其中下一个数据包可能无法长时间到达。

因此,最终,您需要向读者提供有关如何将数据划分为块的知识,这意味着您需要客户端进行解码,这就是基本流类尚未提供数据的原因你试图实施的方式。

所以,通过添加这个中级课,你会获得什么?它最多会给你的I / O增加额外的复杂性和开销(让我们面对它,你试图从客户端代码中抽象出来的只是几行代码)。在最坏的情况下,它将无法根据您的需要将数据分成多个块,因此根本没用。

谨防"This scenario may be highly unlikely to occur in practice":在传输大量数据时,您可以放心,即使是“非常不可能”的事件也会以相当规律的方式发生 - 当然经常会发现您不能认为它们永远不会发生。

[编辑 - 添加]

如果您不想概括您的解决方案,那么您可以在类中轻松添加逻辑来处理问题。

两种可能的解决方案是:

  • 如果您知道将输出给您的控制台线的最大限制,您可以简单地使用足够大的缓冲区,以确保您的边缘情况永远不会发生。 (例如,CreateProcess命令限制为32k,cmd.exe将命令限制为8k。您可能会发现与您收到的数据“块”相似的限制)

  • 如果您的块总是行(换行符终止的文本块),那么只需检查缓冲区中的最后一个字符是否看起来像终结符(0x0a或0x0d)。如果没有,则需要阅读更多数据。

答案 2 :(得分:0)

我倾向于删除“双缓冲”(填充StringBuilder的部分,然后在数据已满时传递数据),并在读取字节时返回从Stream缓冲区接收的数据。所以在ReadHappened中你会得到:

if (bytesRead > 0) {
    this.OnInputRead(); // only send back if we 're sure we got it all
}

正如其他人所说,订阅者需要知道有关消息/数据块的信息以及如何将多个部分组合成一个整体。因此,您可以在收到时返回每个部分。如果订户是一个“愚蠢的订户”,它只是作为一个管道,这也会起作用。