【问题标题】:Reading from a HttpResponseStream fails从 HttpResponseStream 读取失败
【发布时间】:2013-10-08 00:05:49
【问题描述】:

我遇到了从 HttpResponseStream 读取失败的问题,因为我包裹的 StreamReader 读取速度比响应流获得实际响应的速度更快。我正在检索一个相当小的文件(大约 60k),但是将响应处理为实际对象的解析器失败,因为它遇到了一个意外字符(代码 65535),根据经验,我知道这是您从StreamReader 并且没有更多可用的字符。

作为记录,我知道返回的内容是有效的,并且会正确解析,因为每次运行代码时都会在文件中的不同点发生故障。失败的地方是下面的 parser.Load() 行。

有没有办法确保我在尝试解析之前已经阅读了所有内容,而不是将响应流复制到 MemoryStream 或字符串中然后进行处理?

    /// <summary>
    /// Makes a Query where the expected Result is an RDF Graph ie. CONSTRUCT and DESCRIBE Queries
    /// </summary>
    /// <param name="sparqlQuery">SPARQL Query String</param>
    /// <returns>RDF Graph</returns>
    public Graph QueryWithResultGraph(String sparqlQuery)
    {
        try
        {
            //Build the Query URI
            StringBuilder queryUri = new StringBuilder();
            queryUri.Append(this._endpoint.ToString());
            queryUri.Append("?query=");
            queryUri.Append(Uri.EscapeDataString(sparqlQuery));

            if (!this._defaultGraphUri.Equals(String.Empty))
            {
                queryUri.Append("&default-graph-uri=");
                queryUri.Append(Uri.EscapeUriString(this._defaultGraphUri));
            }

            //Make the Query via HTTP
            HttpWebResponse httpResponse = this.DoQuery(new Uri(queryUri.ToString()),false);

            //Set up an Empty Graph ready
            Graph g = new Graph();
            g.BaseURI = this._endpoint;

            //Parse into a Graph based on Content Type
            String ctype = httpResponse.ContentType;
            IRDFReader parser = MIMETypesHelper.GetParser(ctype);
            parser.Load(g, new StreamReader(httpResponse.GetResponseStream()));

            return g;
        }
        catch (UriFormatException uriEx)
        {
            //URI Format Invalid
            throw new Exception("The format of the URI was invalid", uriEx);
        }
        catch (WebException webEx)
        {
            //Some sort of HTTP Error occurred
            throw new Exception("A HTTP Error occurred", webEx);
        }
        catch (RDFException)
        {
            //Some problem with the RDF or Parsing thereof
            throw;
        }
        catch (Exception)
        {
            //Other Exception
            throw;
        }
    }

    /// <summary>
    /// Internal Helper Method which executes the HTTP Requests against the SPARQL Endpoint
    /// </summary>
    /// <param name="target">URI to make Request to</param>
    /// <param name="sparqlOnly">Indicates if only SPARQL Result Sets should be accepted</param>
    /// <returns>HTTP Response</returns>
    private HttpWebResponse DoQuery(Uri target, bool sparqlOnly)
    {
        //Expect errors in this function to be handled by the calling function

        //Set-up the Request
        HttpWebRequest httpRequest;
        HttpWebResponse httpResponse;
        httpRequest = (HttpWebRequest)WebRequest.Create(target);

        //Use HTTP GET/POST according to user set preference
        if (!sparqlOnly)
        {
            httpRequest.Accept = MIMETypesHelper.HTTPAcceptHeader();
            //For the time being drop the application/json as this doesn't play nice with Virtuoso
            httpRequest.Accept = httpRequest.Accept.Replace("," + MIMETypesHelper.JSON[0], String.Empty);
        }
        else
        {
            httpRequest.Accept = MIMETypesHelper.HTTPSPARQLAcceptHeader();
        }
        httpRequest.Method = this._httpMode;
        httpRequest.Timeout = this._timeout;

        //HTTP Debugging
        if (Options.HTTPDebugging)
        {
            Tools.HTTPDebugRequest(httpRequest);
        }

        httpResponse = (HttpWebResponse)httpRequest.GetResponse();

        //HTTP Debugging
        if (Options.HTTPDebugging)
        {
            Tools.HTTPDebugResponse(httpResponse);
        }

        return httpResponse;
    }

编辑

为了澄清我已经说过的,这不是解析器中的错误,这是 StreamReader 读取速度快于响应流提供数据的问题。我可以通过执行以下操作来解决此问题,但希望得到更好或更优雅的解决方案的建议:

            //Parse into a Graph based on Content Type
            String ctype = httpResponse.ContentType;
            IRDFReader parser = MIMETypesHelper.GetParser(ctype);
            Stream response = httpResponse.GetResponseStream();
            MemoryStream temp = new MemoryStream();
            Tools.StreamCopy(response, temp);
            response.Close();
            temp.Seek(0, SeekOrigin.Begin);
            parser.Load(g, new StreamReader(temp));

编辑 2

根据 Eamon 的建议,BlockingStreamReader 类:

/// <summary>
/// A wrapper to a Stream which does all its Read() and Peek() calls using ReadBlock() to handle slow underlying streams (eg Network Streams)
/// </summary>
public sealed class BlockingStreamReader : StreamReader
{
    private bool _peeked = false;
    private int _peekChar = -1;

    public BlockingStreamReader(StreamReader reader) : base(reader.BaseStream) { }

    public BlockingStreamReader(Stream stream) : base(stream) { }

    public override int Read()
    {
        if (this._peeked)
        {
            this._peeked = false;
            return this._peekChar;
        }
        else
        {
            if (this.EndOfStream) return -1;

            char[] cs = new char[1];
            base.ReadBlock(cs, 0, 1);

            return cs[0];
        }
    }

    public override int Peek()
    {
        if (this._peeked)
        {
            return this._peekChar;
        }
        else
        {
            if (this.EndOfStream) return -1;

            this._peeked = true;

            char[] cs = new char[1];
            base.ReadBlock(cs, 0, 1);

            this._peekChar = cs[0];
            return this._peekChar;
        }
    }

    public new bool EndOfStream
    {
        get
        {
            return (base.EndOfStream && !this._peeked);
        }
    }
}

编辑 3

这是一个大大改进的解决方案,它可以包装任何TextReader 并提供EndOfStream 属性。它使用一个内部缓冲区,该缓冲区通过在包装的TextReader 上使用ReadBlock() 填充。阅读器的所有 Read() 方法都可以使用这个缓冲区来定义,缓冲区大小是可配置的:

    /// <summary>
/// The BlockingTextReader is an implementation of a <see cref="TextReader">TextReader</see> designed to wrap other readers which may or may not have high latency.
/// </summary>
/// <remarks>
/// <para>
/// This is designed to avoid premature detection of end of input when the input has high latency and the consumer tries to read from the input faster than it can return data.  All methods are defined by using an internal buffer which is filled using the <see cref="TextReader.ReadBlock">ReadBlock()</see> method of the underlying <see cref="TextReader">TextReader</see>
/// </para>
/// </remarks>
public sealed class BlockingTextReader : TextReader
{
    private char[] _buffer;
    private int _pos = -1;
    private int _bufferAmount = -1;
    private bool _finished = false;
    private TextReader _reader;

    public const int DefaultBufferSize = 1024;

    public BlockingTextReader(TextReader reader, int bufferSize)
    {
        if (reader == null) throw new ArgumentNullException("reader", "Cannot read from a null TextReader");
        if (bufferSize < 1) throw new ArgumentException("bufferSize must be >= 1", "bufferSize");
        this._reader = reader;
        this._buffer = new char[bufferSize];
    }

    public BlockingTextReader(TextReader reader)
        : this(reader, DefaultBufferSize) { }

    public BlockingTextReader(Stream input, int bufferSize)
        : this(new StreamReader(input), bufferSize) { }

    public BlockingTextReader(Stream input)
        : this(new StreamReader(input)) { }

    private void FillBuffer()
    {
        this._pos = -1;
        if (this._finished)
        {
            this._bufferAmount = 0;
        }
        else
        {
            this._bufferAmount = this._reader.ReadBlock(this._buffer, 0, this._buffer.Length);
            if (this._bufferAmount == 0 || this._bufferAmount < this._buffer.Length) this._finished = true;
        }
    }

    public override int ReadBlock(char[] buffer, int index, int count)
    {
        if (count == 0) return 0;
        if (buffer == null) throw new ArgumentNullException("buffer");
        if (index < 0) throw new ArgumentException("index", "Index must be >= 0");
        if (count < 0) throw new ArgumentException("count", "Count must be >= 0");
        if ((buffer.Length - index) < count) throw new ArgumentException("Buffer too small");

        if (this._bufferAmount == -1 || this._pos >= this._bufferAmount)
        {
            if (!this._finished)
            {
                this.FillBuffer();
                if (this.EndOfStream) return 0;
            }
            else
            {
                return 0;
            }
        }

        this._pos = Math.Max(0, this._pos);
        if (count <= this._bufferAmount - this._pos)
        {
            //If we have sufficient things buffered to fufill the request just copy the relevant stuff across
            Array.Copy(this._buffer, this._pos, buffer, index, count);
            this._pos += count;
            return count;
        }
        else
        {
            int copied = 0;
            while (copied < count)
            {
                int available = this._bufferAmount - this._pos;
                if (count < copied + available)
                {
                    //We can finish fufilling this request this round
                    int toCopy = Math.Min(available, count - copied);
                    Array.Copy(this._buffer, this._pos, buffer, index + copied, toCopy);
                    copied += toCopy;
                    this._pos += toCopy;
                    return copied;
                }
                else
                {
                    //Copy everything we currently have available
                    Array.Copy(this._buffer, this._pos, buffer, index + copied, available);
                    copied += available;
                    this._pos = this._bufferAmount;

                    if (!this._finished)
                    {
                        //If we haven't reached the end of the input refill our buffer and continue
                        this.FillBuffer();
                        if (this.EndOfStream) return copied;
                        this._pos = 0;
                    }
                    else
                    {
                        //Otherwise we have reached the end of the input so just return what we've managed to copy
                        return copied;
                    }
                }
            }
            return copied;
        }
    }

    public override int Read(char[] buffer, int index, int count)
    {
        return this.ReadBlock(buffer, index, count);
    }

    public override int Read()
    {
        if (this._bufferAmount == -1 || this._pos >= this._bufferAmount - 1)
        {
            if (!this._finished)
            {
                this.FillBuffer();
                if (this.EndOfStream) return -1;
            }
            else
            {
                return -1;
            }
        }

        this._pos++;
        return (int)this._buffer[this._pos];
    }

    public override int Peek()
    {
        if (this._bufferAmount == -1 || this._pos >= this._bufferAmount - 1)
        {
            if (!this._finished)
            {
                this.FillBuffer();
                if (this.EndOfStream) return -1;
            }
            else
            {
                return -1;
            }
        }

        return (int)this._buffer[this._pos + 1];
    }

    public bool EndOfStream
    {
        get
        {
            return this._finished && (this._pos >= this._bufferAmount - 1);
        }
    }

    public override void Close()
    {
        this._reader.Close();
    }

    protected override void Dispose(bool disposing)
    {
        this.Close();
        this._reader.Dispose();
        base.Dispose(disposing);
    }
}

【问题讨论】:

  • 所以,在它推出九年后,您碰巧是世界上第一个发现StreamReader 的阅读速度比它本应阅读的Stream 快的人,对吗?
  • 不,我只是想知道是否有人有比上述更优雅的解决方案
  • 解决方案是什么? StreamReader 的读取速度并不比 Stream 快。
  • 代码中的错误。请记住:总是是你的错。这绝不是操作系统或框架的错。
  • 你真的没有道理,你的第一条评论表明 StreamReader 速度太快,然后你说你根本没有真正帮助我解决这个问题

标签: c# .net stream httpresponse streamreader


【解决方案1】:

在不知道您使用的解析器的细节的情况下,我只能猜测错误,但有一个相当容易产生错误的 .NET 框架 I/O 库几乎鼓励您制作...

您是否知道 Streams 和 TextReader 读取的字节/字符可能比请求的少?

特别是 TextReader.Read(char[] buffer, int index, int count) 的文档说:

返回值

类型:System..::.Int32

已读取的字符数。该数字将小于或等于计数,具体取决于数据是否在流中可用。如果在没有更多字符要读取时调用此方法,则返回零。

强调我的。

例如,如果您调用 reader.Read(buffer, 0, 100) 您不能假设已读取 100 个字符。

编辑:解析器很可能确实假设了这一点; 这解释了你观察到的行为:如果你将流完全缓存在 MemoryStream 中,总会有足够的字符来填充请求 - 但如果你不这样做,解析器将收到比请求更少的字符每当底层流“慢”时,在不可预测的时间。

Edit2:您可以通过将解析器中的所有 TextReader.Read() 实例替换为 TextReader.ReadBlock() 来修复您的错误。

【讨论】:

  • 我知道这一点,我不确定它是否需要算作 StreamReader 中的一个错误,更多的只是当底层流可能很慢时它的行为方式。解析器不是问题,如果我使用第二个代码片段(添加到原始问题),它在解析之前读取整个 Stream 解析很好
  • 解析器中的一个错误,可能性很高。按照设计,如果底层流“慢”,则流读取器返回的字符数少于请求的字符数。使用 memorystream 作为底层流会导致 streamreader 始终返回完整数量的字符 - 解决解析器中的错误。
  • 解析器使用底层标记器,它使用 Read() 方法逐个字符地读取,因此您很可能是对的,我将测试 ReadBlock() 事情并接受您的答案,如果证明可以解决问题
  • ReadBlock() 方法证明不能完全解决我的问题,因为即使我使用它,我仍然需要对 Peek() 进行大量调用,这与 Read() 遇到相同的问题
  • 那么你有三个选择:(1)只需将整个流预缓存在内存流中; (2) 实现您自己的 TextReader 子类,在 Peek() 和 Read() 上包装另一个 TextReader 和块(这实际上很简单;您只需要在 ReadBlock 方面实现 Peek+Read),或 (3) 替换对 Peek 的调用使用 ReadBlock 填充本地单字符前瞻字符(您需要确保在下次阅读时手动包含该字符。我更喜欢选项(2)。
【解决方案2】:

要支持阻塞读取场景,而不是子类化 StreamReader,您可以子类化 TextReader:这避免了 EndOfStream 的问题,这意味着您可以使 任何 读取器阻塞 - 不是只是StreamReaders:

public sealed class BlockingReader : TextReader
{
    bool hasPeeked;
    int peekChar;
    readonly TextReader reader;

    public BlockingReader(TextReader reader) { this.reader = reader; }

    public override int Read()
    {
        if (!hasPeeked)
            return reader.Read();
        hasPeeked = false;
        return peekChar;
    }

    public override int Peek()
    {
        if (!hasPeeked)
        {
            peekChar = reader.Read();
            hasPeeked = true;
        }
        return peekChar;
    }

    public override int Read(char[] buffer, int index, int count)
    {
        if (buffer == null)
            throw new ArgumentNullException("buffer");
        if (index < 0)
            throw new ArgumentOutOfRangeException("index");
        if (count < 0)
            throw new ArgumentOutOfRangeException("count");
        if ((buffer.Length - index) < count)
            throw new ArgumentException("Buffer too small");

        int peekCharsRead = 0;
        if (hasPeeked)
        {
            buffer[index] = (char)peekChar;
            hasPeeked = false;
            index++;
            count--;
            peekCharsRead++;
        }

        return peekCharsRead + reader.ReadBlock(buffer, index, count);
    }

    protected override void Dispose(bool disposing)
    {
        try
        {
            if (disposing)
                reader.Dispose();
        }
        finally
        {
            base.Dispose(disposing);
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-30
    • 1970-01-01
    • 2014-12-26
    • 2015-06-10
    • 2015-08-26
    • 1970-01-01
    • 2011-04-14
    • 1970-01-01
    相关资源
    最近更新 更多