下载大文件时,SendAsync和CopyToAsync不起作用

时间:2018-09-24 22:30:31

标签: c# asp.net-core .net-core reverse-proxy

我有一个小应用程序,它接收来自浏览器的请求,复制接收到的标头和发布数据(或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

4 个答案:

答案 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.
    }
  }
}

LEARN MORE HERE

答案 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)
}