SendAsync и CopyToAsync не работают при загрузке большого файла - PullRequest
0 голосов
/ 25 сентября 2018

У меня есть небольшое приложение, которое получает запрос от браузера, копирует полученный заголовок и данные сообщения (или путь GET) и отправляет его на другую конечную точку.

Затем ждет результата и отправляет его обратно в браузер.Это работает как обратный прокси.

Все работает нормально, пока не получит запрос на загрузку большого файла.Что-то вроде 30 МБ вызовет странное поведение в браузере.Когда браузер достигает около 8 МБ, он перестает получать данные из моего приложения и через некоторое время прерывает загрузку.Все остальное работает просто отлично.

Если я поменяю строку 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 для ожидания responseMessage.Content.ReadAsStreamAsync с помощью оператора await.

Ответы [ 4 ]

0 голосов
/ 02 октября 2018

На рисунке 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 по одному блоку за раз, поэтому, когда await завершает свою работу, он планирует оставшуюся часть асинхронного метода в потоке пула потоков.Метод может завершиться, что завершает возвращаемое задание, и нет тупика.Эта разница в поведении может сбивать с толку, когда программисты пишут тестовую консольную программу, наблюдают, как частично асинхронный код работает должным образом, а затем перемещают тот же код в приложение с графическим интерфейсом или ASP.NET, где оно блокируется.

Лучшее решение этой проблемы - позволить асинхронному коду расти естественным образом через кодовую базу.Если вы последуете этому решению, вы увидите расширение асинхронного кода до точки входа, обычно это обработчик событий или действие контроллера.Консольные приложения не могут полностью следовать этому решению, потому что метод Main не может быть асинхронным.Если метод Main был асинхронным, он мог вернуться до завершения, что привело к завершению программы.На рисунке 4 показано это исключение из руководства. Метод Main для консольного приложения является одной из немногих ситуаций, когда код может блокироваться в асинхронном методе.

Рисунок 4 Метод Main может вызывать Task.Wait или Task.Результат

class Program
{
  static void Main()
  {
    MainAsync().Wait();
  }
  static async Task MainAsync()
  {
    try
    {
      // Asynchronous implementation.
      await Task.Delay(1000);
    }
    catch (Exception ex)
    {
      // Handle exceptions.
    }
  }
}

УЗНАТЬ БОЛЬШЕ ЗДЕСЬ

0 голосов
/ 27 сентября 2018

попробуйте это.

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)
}
0 голосов
/ 02 октября 2018

Вы пытаетесь передать файл, но вы делаете это не совсем правильно.Если вы не укажете, 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..
    }
}
0 голосов
/ 27 сентября 2018

Просто предположение, но я полагаю, что проблема заключается в удалении кодировки передачи:

context.Response.Headers.Remove("transfer-encoding");

Если запрос http, который вы делаете с помощью _httpClient, возвращает файл 30 МБ, используя Chunked *Кодировка 1006 * (целевой сервер не знает размер файла), то вам нужно будет вернуть файл в браузер с кодировкой Chunked .

Когда вы буферизируете ответ на своем веб-сервисе (передавая 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);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...