我正在使用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
。
因此,调用代码似乎需要了解并拥有响应消息和流的所有权,或者我保留响应消息未处理并让终结器处理它。这两种选择都不对。
这个答案谈到不处置HttpClient
.HttpRequestMessage
和/或HttpResponseMessage
怎么样?
我错过了什么吗?我希望使消费代码对HTTP一无所知,但是留下所有这些未处理的对象违背了一年的习惯!
所以似乎调用代码需要知道并采取 响应消息和流的所有权,否则我离开 响应消息未释放,并让终结器处理它。 这两种选择都不对。
在此特定情况下,没有终结器。HttpResponseMessage
和HttpRequestMessage
都没有实现终结器(这是一件好事!如果您不处理其中任何一个,一旦 GC 启动,它们就会被垃圾收集,一旦发生这种情况,它们底层流的句柄将被收集。
只要您使用这些对象,就不要释放。完成后,处理掉它们。完成后,您始终可以显式调用Dispose
,而不是将它们包装在 using
语句中。无论哪种方式,使用代码都不需要具有任何底层 http 请求的知识。
在 .NET 中处理一次性文件既简单又困难。确实。
流拉着同样的废话...释放缓冲区是否也会自动释放它包装的流?应该吗?作为消费者,我甚至应该知道它是否如此?
当我处理这些东西时,我会遵守一些规则:
- 如果我认为有非本地资源在起作用(比如,网络连接!),我永远不会让GC"绕过它"。资源耗尽是真实存在的,好的代码会处理它。
- 如果一次性使用一次性作为参数,那么覆盖我的屁股并确保我的代码处理它所做的每个对象永远不会有什么坏处。如果我的代码没有成功,我可以忽略它。
- GC 调用 ~Finalize,但没有什么能保证 Finalize(即您的自定义析构函数)调用 Dispose。与上述观点相反,没有魔法,所以你必须对它负责。
所以,你有一个HttpClient,一个HttpRequestMessage和一个HttpResponseMessage。他们每个人的寿命,以及他们制造的任何一次性产品,都必须得到尊重。因此,您的Stream
永远不应该期望在HttpResponseMessage
的一次性生存期之外存活,因为您没有实例化流。
在上面的场景中,我的模式是假装获取该Stream
实际上只是在 Static.DoGet(uri) 方法中,而您返回的流必须是我们自己制作的。这意味着第二个流,与HttpResponseMessage
的流。复制我的新流(通过FileStream
或MemoryStream
或任何最适合您情况的路由)...或类似的东西,因为:
- 您无权使用
HttpResponseMessage's
Stream的生命周期。那是他的,不是你的。:) - 像
HttpClient
这样的一次性物品的使用寿命,而你却在处理返回的流的内容物时,是一个疯狂的阻碍者。这就像在解析数据表时保持SqlConnection
(想象一下,如果数据表变得很大,我们会多快饿死一个连接池) - 公开如何获得该响应可能会对 SOLID 不利......你有一个 Stream,它是一次性的,但它来自一个
HttpResponseMessage
,这是一次性的,但这只是因为我们使用了HttpClient
和HttpRequestMessage
,它们是一次性的......你想要的只是来自 URI 的流。这些责任有多令人困惑? - 网络仍然是计算机系统中最慢的通道。将它们用于"优化"仍然是疯狂的。总是有更好的方法来处理最慢的组件。
因此,请使用诸如捕获和释放之类的一次性用品...制作它们,为自己获取结果,尽快发布它们。并且不要混淆正确性的优化,尤其是来自不是您自己编写的类。
您还可以将流作为输入参数,以便调用方可以完全控制流的类型及其处置。现在,您还可以在控件离开该方法之前释放 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);
}
}
不要释放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");
}
经过几个小时的思考,我得出的结论是这种方法是最好的:
将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);
重要的是,d提出这个包装器也会释放响应,能够有效地控制生命周期。
通过这种方式,您可以安全地创建像此方法这样的泛型使用者,这些使用者不关心底层实现的任何内容:
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
方法完全忽略了与处置相关的不满。它只是像任何其他流一样处理流,并且处置在内部处理。
我希望这有所帮助。