.net 调用ReadAsStreamAsync时何时或是否处置HttpResponseMessage?

sshcrbum  于 2023-03-09  发布在  .NET
关注(0)|答案(5)|浏览(244)

我正在使用System.Net.Http.HttpClient进行一些客户端HTTP通信。我将所有HTTP放在一个位置,与其余代码分离。在一个示例中,我希望以流的形式读取响应内容,但是流的消费者与HTTP通信发生和流被打开的地方很好地隔离。在负责HTTP通信的地方,我处理了所有的HttpClient东西。
此单元测试将在Assert.IsTrue(stream.CanRead)处失败:

[TestMethod]
public async Task DebugStreamedContent()
{
    Stream stream = null; // in real life the consumer of the stream is far away 
    var client = new HttpClient();        
    client.BaseAddress = new Uri("https://www.google.com/", UriKind.Absolute);

    using (var request = new HttpRequestMessage(HttpMethod.Get, "/"))
    using (var response = await client.SendAsync(request))
    {
        response.EnsureSuccessStatusCode();
        //here I would return the stream to the caller
        stream = await response.Content.ReadAsStreamAsync();
    }

    Assert.IsTrue(stream.CanRead); // FAIL if response is disposed so is the stream
}

我通常会尽可能早地处理掉IDisposable中的任何内容,但在本例中,处理HttpResponseMessage也会处理掉从ReadAsStreamAsync返回的Stream
因此,似乎调用代码需要知道响应消息和流并获得它们的所有权,或者我不处理响应消息,让终结器来处理它。
This answer谈到不处理HttpClient,那么HttpRequestMessage和/或HttpResponseMessage呢?
我错过了什么吗?我希望让消费代码不知道HTTP,但留下所有这些未处理的对象违背了多年的习惯!

wpx232ag

wpx232ag1#

因此,似乎调用代码需要知道响应消息和流并获得它们的所有权,或者我不处理响应消息,让终结器来处理它。
在这个特定的例子中,* 没有终结器 *。HttpResponseMessageHttpRequestMessage都没有实现终结器(这是一件好事!)。如果你不处理它们中的任何一个,一旦GC启动,它们就会被垃圾收集,一旦发生这种情况,它们底层流的句柄就会被收集。
只要你在使用这些对象,就不要dispose。一旦完成,就 dispose of them。不用把它们 Package 在using语句中,你总是可以在完成后显式调用Dispose。无论哪种方式,使用代码都不需要了解http请求的底层知识。

f4t66c6m

f4t66c6m2#

你也可以把stream作为输入参数,这样调用者就可以完全控制流的类型和处理,现在你也可以在控制权离开方法之前处理httpResponse。
下面是HttpClient的扩展方法

public static async Task HttpDownloadStreamAsync(this HttpClient httpClient, string url, Stream output)
    {
        using (var httpResponse = await httpClient.GetAsync(url).ConfigureAwait(false))
        {
            // Ensures OK status
            response.EnsureSuccessStatusCode();

            // Get response stream
            var result = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);

            await result.CopyToAsync(output).ConfigureAwait(false);
            output.Seek(0L, SeekOrigin.Begin);                
        }
    }
eaf3rand

eaf3rand3#

在.NET中处理一次性的东西既容易又困难。
流也是一样的废话......释放缓冲区也会自动释放它 Package 的流吗?应该吗?作为使用者,我甚至应该知道它是否会这样做吗?
当我处理这些事情的时候,我有一些规则:
1.如果我认为有非本机资源在起作用(比如网络连接),我绝不会让GC“去处理它”。资源耗尽是真实的的,好的代码会处理它。
1.如果Disposable将Disposable作为参数,那么保护代码并确保代码处理它生成的每一个对象是没有坏处的。如果代码没有处理,我可以忽略它。

  1. GC调用~Finalize,但是没有任何东西可以保证Finalize(也就是你的自定义析构函数)调用Dispose。与上面的观点相反,这里没有魔法,所以你必须对它负责。
    因此,您有一个HttpClient、一个HttpRequestMessage和一个HttpResponseMessage。它们中的每一个以及它们所创建的任何Disposable的生存期都必须得到尊重。因此,您的Stream不应该在HttpResponseMessage的Disposable生存期之外继续存在,因为 * 您 * 没有示例化Stream。
    在上面的场景中,我的模式是假设获取Stream实际上只是在Static.DoGet中(uri)方法,并且您返回的Stream必须是我们自己创建的。这意味着第二个Stream,CopyTo我的新流(通过FileStreamMemoryStream或任何最适合您的情况的路由)...或类似的东西,因为:
  • 你没有权利拥有HttpResponseMessage's Stream的生命。那是他的,不是你的。:)
  • 当您处理返回流的内容时,占用HttpClient这样的可释放对象的生存期是一个疯狂的阻塞器,这就像在解析DataTable时占用SqlConnection一样(想象一下,如果DataTable变得很大,我们会多么快地耗尽连接池)
  • 公开获取响应的“方式”可能会对SOLID不利......您有一个Stream,它是一次性的,但它来自一个HttpResponseMessage,它是一次性的,但这只是因为我们使用了HttpClientHttpRequestMessage,它们是一次性的......而您想要的只是一个来自URI的流。
  • 网络仍然是计算机系统中最慢的通道。阻止它们进行“优化”仍然是疯狂的。总是有更好的方法来处理最慢的组件。

因此,使用像捕捉和释放这样的一次性工具......制作它们,自己捕捉结果,尽快释放它们。不要把优化与正确性混为一谈,尤其是对于那些不是你自己编写的类。

vnjpjtjt

vnjpjtjt4#

不要释放HttpResponseMessage,因为这是调用此方法的一方的职责。

方法:

public async Task<Stream> GetStreamContentOrNullAsync()
{
    // The response will be disposed when the returned content stream is disposed.
    const string url = "https://myservice.com/file.zip";
    var client = new HttpClient(); //better use => var httpClient = _cliHttpClientFactory.CreateClient();
    var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
    if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
    {
        return null;
    }

    return await response.Content.ReadAsStreamAsync();
}

用法:

public async Task<IActionResult> DownloadPackageAsync()
  {
      var stream = await GetStreamContentOrNullAsync();
      if (stream == null)
      {
            return NotFound();
      }

      return File(stream, "application/octet-stream");
  }
uyhoqukh

uyhoqukh5#

经过几个小时的思考,我得出了这样一个结论:
一个适配器,它将HttpRequestMessage及其内容流作为依赖项。
这就是它。请密切注意它的静态工厂方法Create。由于显而易见的原因,构造函数是私有的。

public class HttpResponseMessageStream : Stream
{
    private readonly HttpResponseMessage response;

    private readonly Stream inner;

    private HttpResponseMessageStream(Stream stream, HttpResponseMessage response)
    {
        inner = stream;
        this.response = response;
    }

    public override bool CanRead => inner.CanRead;

    public override bool CanSeek => inner.CanSeek;

    public override bool CanWrite => inner.CanWrite;

    public override long Length => inner.Length;

    public override long Position
    {
        get => inner.Position;
        set => inner.Position = value;
    }

    public static async Task<HttpResponseMessageStream> Create(HttpResponseMessage response)
    {
        return new HttpResponseMessageStream(await response.Content.ReadAsStreamAsync(), response);
    }

    public override ValueTask DisposeAsync()
    {
        response.Dispose();
        return base.DisposeAsync();
    }

    public override void Flush()
    {
        inner.Flush();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return inner.Read(buffer, offset, count);
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        return inner.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
        inner.SetLength(value);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        inner.Write(buffer, offset, count);
    }

    protected override void Dispose(bool disposing)
    {
        response.Dispose();
        base.Dispose(disposing);
    }
}

检查此示例用法:

HttpRequestMessage response = // Obtain the message somewhere, like HttpClient.GetAsync()
var wrapperStream = await HttpResponseMessageStream.Create(response);

重要的是,处理这个 Package 器也会处理响应,从而能够有效地控制生命周期。
这样,你就可以安全地创建像这个方法一样的泛型消费者,它不关心底层实现的任何事情:

public async Task DoSomething(Func<Task<Stream>> streamFactory) 
{
    using (var stream = await streamFactory())
    {
       ...
    }
}

像这样使用它:

async Task<Stream> GetFromUri(Uri uri)
{
    var response = ...
    return await HttpResponseMessageStream.Create(response);
}

await DoSomething(() => GetFromUri("http://...");

DoSomething方法完全忽略了与处理相关的不满,它只是像处理其他流一样处理流,并且处理在内部进行。
希望这能帮上忙。

相关问题