如何异步且可取消地复制HttpContent?

11
我正在使用 HttpClient.PostAsync() 方法,响应是一个 HttpResponseMessage 对象。它的 Content 属性是一个 HttpContent 对象,其中包含一个 CopyToAsync() 方法,但这个方法不支持取消操作。
有没有一种方法可以将响应内容复制到一个 Stream 流中并传递一个 CancellationToken 取消标记?
我并没有被困在 CopyToAsync() 方法里!如果有解决方法的话,那就好了。比如读取几个字节,检查是否已取消,然后继续读取等等。 HttpContent.CreateContentReadStreamAsync() 方法似乎是一个可行的选择。不幸的是,它在我选择的框架中不可用。而且也不确定它是否会一次性读取所有数据,并浪费大量内存。
注意:我正在 PCL 中使用这个方法,目标平台为 WP8、Windows Store 8、.NET 4.5、Xamarin.iOS 和 Xamarin.Android。
3个回答

19

我相信这应该可以工作:

public static async Task DownloadToStreamAsync(string uri, HttpContent data, Stream target, CancellationToken token)
{
    using (var client = new HttpClient())
    using (var response = await client.PostAsync(uri, data, token))
    using (var stream = await response.Content.ReadAsStreamAsync())
    {
        await stream.CopyToAsync(target, 4096, token);
    }
}

请注意,ReadAsStreamAsync 方法调用 CreateContentReadStreamAsync 方法,在处理流响应时,后者会返回底层内容流而不将其缓冲到内存中。


5
我已经采用了这种方法,确保响应内容是流式的,使用SendAsync而不是PostAsync。使用SendAsync时,必须使用HttpCompletionOption.ResponseHeadersRead参数,否则SendAsync调用不会返回,直到读取完整个响应,包括所有内容(MB、GB等),即不可取,因为所有数据都被读入内存(可以在任务管理器中看到所使用的内存)。使用HttpCompletionOption.ResponseHeadersRead意味着可以正确地流式传输数据。我不确定PostAsync是用什么方式实现的,但希望这些信息对其他人有所帮助。 - cbailiss
httpClient.GetStreamAsyncContent.ReadAsStreamAsync 返回的流上调用 CopyToAsync 方法时会忽略令牌。详细信息 - SerG

2
您无法取消不可取消的操作。请参阅如何取消不可取消的异步操作?
但是,您可以使用WithCancellation使您的代码流程表现得好像底层操作已被取消。
public static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    return task.IsCompleted
        ? task
        : task.ContinueWith(
            completedTask => completedTask.GetAwaiter().GetResult(),
            cancellationToken,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
}

使用方法:

await httpContent.PostAsync(stream).WithCancellation(new CancellationTokenSource(1000).Token);

这是可怕的事情。如果我在响应仍在读取时取消会发生什么?假设流正在进入文件中。用户取消然后再次下载相同的内容。正如链接文章中所述,各种各样的问题可能会出现。 - Krumelur
这是非常正确的。这也可能是为什么没有一个重载函数接受取消标记,以及为什么WithCancellation不是.Net的一部分。但正如我所说,如果开发人员没有给你提供取消异步操作的方法,你就无法取消它。你只能切断它并继续执行,可能会捕获ObjectDisposedException或有时是NullReferenceException。 - i3arnon

1

HttpClient.CancelPendingRequests预期将取消所有待处理的操作。但我没有验证是否也适用于CopyToAsync。请随意尝试:

public static async Task CopyToAsync(
    System.Net.Http.HttpClient httpClient, 
    System.Net.Http.HttpContent httpContent,
    Stream stream, CancellationToken ct)
{
    using (ct.Register(() => httpClient.CancelPendingRequests()))
    {
        await httpContent.CopyToAsync(stream);
    }
}

[更新] 经过验证:不幸的是,这并不能取消 HttpContent.CopyToAsync。我仍然保留了这个答案,因为这个模式本身可能对取消 HttpClient 上的其他操作有用。


@down-voter:你的投票是否意味着CancelPendingRequestsCopyToAsync没有影响? - noseratio - open to work
1
是的,对此我感到抱歉。CancelPendingRequests 只在实际请求/响应期间起作用。 - Darrel Miller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接