我有一个小应用程序,它接收来自浏览器的请求,复制接收到的标头和发布数据(或GET路径),并将其发送到另一个端点。
然后它等待结果并将其发送回浏览器。它像反向代理一样工作。
一切正常,直到收到下载大文件的请求。 30MB之类的东西会在浏览器中引起奇怪的行为。当浏览器达到8MB左右时,它将停止从我的应用程序接收数据,并在一段时间后中止下载。其他一切都很好。
如果我将SendAsync
行更改为使用HttpCompletionOption.ResponseContentRead
,它将正常工作。我以为在等待流和/或任务时出了点问题,但是我不知道发生了什么。
该应用程序是用C#.net Core(最新版本)编写的。
这是代码(部分)
private async Task SendHTTPResponse(HttpContext context, HttpResponseMessage responseMessage)
{
context.Response.StatusCode = (int)responseMessage.StatusCode;
foreach (var header in responseMessage.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
foreach (var header in responseMessage.Content.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
context.Response.Headers.Remove("transfer-encoding");
using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
{
await responseStream.CopyToAsync(context.Response.Body);
}
}
public async Task ForwardRequestAsync(string toHost, HttpContext context)
{
var requestMessage = this.BuildHTTPRequestMessage(context);
var responseMessage = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
await this.SendHTTPResponse(context, responseMessage);
}
编辑
已更改SendHTTPResponse以使用await运算符等待responseMessage.Content.ReadAsStreamAsync
。
答案 0 :(得分:0)
只是个猜测,但我认为问题在于转移编码的删除:
context.Response.Headers.Remove("transfer-encoding");
如果您使用_httpClient
发出的http请求使用 Chunked 编码返回了30MB文件(目标服务器不知道文件大小),则需要将文件返回到浏览器也使用 Chunked 编码。
当您在Web服务上缓冲响应(通过传递HttpCompletionOption.ResponseContentRead
)时,您知道要发送回浏览器的确切消息大小,因此响应可以成功工作。
我将检查您从responseMessage
获得的响应标头,以查看传输编码是否已分块。
也只是观察,而是为了正确利用线程池,ForwardRequestAsync()
应该返回一个任务,而不是像这样等待SendHTTPResponse
:
private async Task SendHTTPResponse(HttpContext context, HttpResponseMessage responseMessage)
{
context.Response.StatusCode = (int)responseMessage.StatusCode;
foreach (var header in responseMessage.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
foreach (var header in responseMessage.Content.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
context.Response.Headers.Remove("transfer-encoding");
using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
{
return responseStream.CopyToAsync(context.Response.Body);
}
}
public async Task ForwardRequestAsync(string toHost, HttpContext context)
{
var requestMessage = this.BuildHTTPRequestMessage(context);
var responseMessage = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
return this.SendHTTPResponse(context, responseMessage);
}
答案 1 :(得分:0)
您正在尝试流式传输文件,但是这样做并不完全正确。如果您未指定ResponseHeadersRead
,则除非服务器终止请求,否则响应将永远不会返回,因为服务器将尝试读取响应直到结束。
HttpCompletionOption枚举类型有两个成员,其中之一是ResponseHeadersRead,它告诉HttpClient仅读取标头,然后立即返回结果。
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
var stream = await response.Content.ReadAsStreamAsync();
using (var reader = new StreamReader(stream)) {
while (!reader.EndOfStream) {
//Oh baby we are streaming
//Do stuff copy to response stream etc..
}
}
答案 2 :(得分:0)
图3显示了一个简单的示例,其中一个方法阻塞了异步方法的结果。这段代码在控制台应用程序中可以很好地工作,但是当从GUI或ASP.NET上下文调用时将死锁。这种行为可能会造成混淆,尤其是考虑到逐步调试器意味着永远无法完成的等待。导致死锁的实际原因是在调用Task.Wait时在调用堆栈中更进一步。
图3阻止异步代码时的常见死锁问题
public static class DeadlockDemo
{
private static async Task DelayAsync()
{
await Task.Delay(1000);
}
// This method causes a deadlock when called in a GUI or ASP.NET context.
public static void Test()
{
// Start the delay.
var delayTask = DelayAsync();
// Wait for the delay to complete.
delayTask.Wait();
}
}
此死锁的根本原因是由于等待处理上下文的方式。默认情况下,当等待不完整的任务时,将捕获当前的“上下文”,并在任务完成时将其用于恢复该方法。除非为空,否则此“上下文”是当前的SynchronizationContext,在这种情况下,它是当前的TaskScheduler。 GUI和ASP.NET应用程序具有SynchronizationContext,该上下文一次只允许运行一块代码。当等待完成时,它将尝试在捕获的上下文中执行异步方法的其余部分。但是该上下文中已经有一个线程,该线程(同步)在等待异步方法完成。他们每个人都在等待对方,导致死锁。
请注意,控制台应用程序不会导致此死锁。它们具有线程池SynchronizationContext,而不是一次一次的SynchronizationContext,因此,在等待完成时,它将在线程池线程上调度异步方法的其余部分。该方法能够完成,从而完成了返回的任务,并且没有死锁。当程序员编写测试控制台程序,观察部分异步的代码是否按预期工作,然后将相同的代码移入GUI或ASP.NET应用程序时,这种行为上的差异可能会令人困惑。
此问题的最佳解决方案是允许异步代码通过代码库自然增长。如果您遵循此解决方案,则会看到异步代码扩展到其入口点,通常是事件处理程序或控制器操作。控制台应用程序无法完全遵循此解决方案,因为Main方法不能是异步的。如果Main方法是异步的,则它可能在完成之前返回,从而导致程序结束。图4展示了该准则的例外情况:控制台应用程序的Main方法是少数几种可能在异步方法上阻塞代码的情况之一。
图4主要方法可以调用Task.Wait或Task.Result
class Program
{
static void Main()
{
MainAsync().Wait();
}
static async Task MainAsync()
{
try
{
// Asynchronous implementation.
await Task.Delay(1000);
}
catch (Exception ex)
{
// Handle exceptions.
}
}
}
答案 3 :(得分:-4)
尝试这些。
using (HttpResponseMessage responseMessage= await client.SendAsync(request))
{
await this.SendHTTPResponse(context, responseMessage);
}
或
using (HttpResponseMessage responseMessage=await _httpClient.SendAsync(requestMessage,
HttpCompletionOption.ResponseHeadersRead, context.RequestAborted))
{
await this.SendHTTPResponse(context, responseMessage)
}