1

Подскажите как я могу сделать чтобы в рамках одного запроса через httpclient я мог передать составной запрос из нескольких файлов в потоке. Сейчас я могу передать в потоке один файл и сохранить его на сервере.

Код для отправки файла со стороны клиента

public async Task SendFile(string filePath, string url)
        {
            using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                using (HttpClient client = new HttpClient())
                {
                    client.Timeout = TimeSpan.FromMinutes(10);
                    // Создаем объект MultipartFormDataContent для передачи файла
                    using (MultipartFormDataContent content = new MultipartFormDataContent())
                    {
                        // Создаем объект StreamContent для передачи содержимого файла
                        using (StreamContent fileContent = new StreamContent(fileStream))
                        {
                            client.DefaultRequestHeaders.Add("Token", "1234");
                            content.Add(fileContent, "file", System.IO.Path.GetFileName(filePath));
                        // Отправляем запрос на сервер с помощью метода HTTP POST
                        HttpResponseMessage response = await client.PostAsync(url, content);

                        // Получаем ответ от сервера и проверяем его статусный код
                        if (response.IsSuccessStatusCode)
                        {
                            Console.WriteLine("Файл успешно передан.");
                        }
                        else
                        {
                            Console.WriteLine("Произошла ошибка при передаче файла.");
                        }
                    }
                }
            }
        }
    }

Код для сохранения файла с потока

 [HttpPost("/uploadfile")]
        //[DisableRequestSizeLimit]
        [RequestSizeLimit((long)1e+10)]
        [RequestFormLimits(MultipartBodyLengthLimit = (long)1e+10)]
        public async Task<IActionResult> UploadFile()
        {
            if (!Request.HasFormContentType || !MediaTypeHeaderValue.TryParse(Request.ContentType, out var mediaTypeHeader) || string.IsNullOrEmpty(mediaTypeHeader.MediaType))
                return new UnsupportedMediaTypeResult();
            Stopwatch stopwatch = Stopwatch.StartNew();
            using var fileStream = new FileStream("test.rar", FileMode.Create);
            var buffer = new byte[1024 * 50];
            int bytesRead;
            while ((bytesRead = await Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0)
            {
                await fileStream.WriteAsync(buffer, 0, bytesRead);
            }
            stopwatch.Stop();
            Console.WriteLine($"Время работы сохранения файла: {stopwatch.ElapsedMilliseconds}");
            return Ok();
        }
xellan
  • 560
  • 3
  • 11
  • Почему бы с клиента не выполнить несколько параллельных запросов? Ведь даже если вы накидаете в одну мультипарт-форму запроса несколько файлов, загрузка последовательно пойдет один за другим. В случае с HTTP/1.1 выгоднее слать несколько параллельных запросов, а в случае HTTP/2 хуже точно не будет. И кстати, почему вы считаете, что Request.Body это файл? Там же мультипарт-форма, вы распаковывать полученное пробовали? – aepot Jun 26 '23 at 21:03
  • @aepot принял, о таком варианте тоже думал чтобы паралельно слать запросы или циклом файлы передавать последовательно, хотя с этим кодом хоть и в потоке передаю, то в студии при тестирование нагрузка все равно большая идёт. Попробую развернуть iis локально если также будет нагрузка идти, то код не правильно написал. Request.Body это Stream поток. И в начале метода у меня идет проверка на multypart, да я распаковывал, файл целый на выходе. – xellan Jun 26 '23 at 21:08
  • https://ru.stackoverflow.com/q/1303748/373567 вот, посмотрите. Имя файла научитесь только через запрос передавать, а не хардкодом. Для экономии ресурсов используйте один и тот же HttpClient для отправки, не создавайте новый клиент на каждый запрос, он для этого не предназначен, об этом написано в документации по HttpClient. Время работы вы замеряете в отладочной сборке, замерять надо в релизной. И использовать для этого 2 разные машины, а не одну и ту же. Вместо копирования через буфер можно использовать .CopyToAsync – aepot Jun 26 '23 at 21:13
  • @aepot Если сохранять через var formCollection = await Request.ReadFormAsync(); и сохранять как .CopyToAsync то скорость записи достигает до 700мб\с через буфер скорость записи до 55мб\с и через буфер нагрузка и в отладке меньше идет чем через CopyToAsync и в мсдн читал что свыше 64кб данные кешируются во временные файлы для обработки к примеру от туда и скорость в 700мб у процесса. Ваш код по ссылке читал раннее, хороший материал для изучения. И использовать для этого 2 разные машины, А почему на одной не желательно? – xellan Jun 26 '23 at 21:45
  • Ничего не понял, вы выбрали способ в 10 раз медленнее ради нагрузки? Это ошибка. – aepot Jun 27 '23 at 06:02
  • @aepot почему же способ плохой, большие файлы и передают в потоковой передаче, так не будет файл озу забивать, и способ где запись со скоростью 700мб/с в stopwatch он не быстрее, а нагрузка в 700мб идет так как данные кешируются без буфера – xellan Jun 27 '23 at 11:57
  • Как это 700 не быстрее чем 55? У меня математика не сходится. CopyToAsync внутри себя использует точно такой же буфер и цикл. Только там оптимизаций внутри побольше, чем в вашем коде. Но если вы считаете, что ваш код эффективнее, чем написаный профессионалами внутри .NET библиотек - ваше право. – aepot Jun 27 '23 at 12:09
  • @aepot значит я был не прав. Буду разбираться дальше как оптимизировать ОЗУ, сейчас она может достигать до 300мб при загрузке файла что очень много, когда у меня буфер 50кб, а у CopyToAsync по умолчанию если не ошибаюсь 64кб буфер. Кстати при FileStream же мы пишем в файл по идее без буферизации данных? – xellan Jun 27 '23 at 12:20
  • 300 мегабайт для сервера - это не много. Это нормально, ничего необычного. Если вы сделаете буфер 128кб, это не значит что станет 600 мегабайт, это значит что станет 300+64кб, грубо говоря. К тому же вы смотрите на память в отладчике, в реальности на собраном сервере в релизной сборке и без отладчика будет в 3-4 раза меньше потребление. Запись в файл идет тоже через буфер, это для ускорения, но размер его, если мне память не изменяет, около 80 килобайт по умолчанию. – aepot Jun 27 '23 at 14:02
  • @aepot верно, да 80кб размер по умолчанию, но читал все что свыше 64кб на сервере у с# кешируется во временные файлы для работы, попробую еще с буфером поиграть и думаю на этом будет достаточно проб. – xellan Jun 27 '23 at 16:35
  • Ничего такого подобного нет. Массив создаете в памяти, он в памяти и живет, никакой магии. – aepot Jun 27 '23 at 16:37
  • @aepot в целом, да, но я про потоковую передачу чтобы в памяти не было больше чем 64кб к примеру, то есть сохранять большие файлы без нагрузки. Попробую позже через iis развернуть, в Гугле толком нет информации о потоковой передаче, а в плане производительности тот же Request.Read если не ошибаюсь, перед получением информации о файле стоит в ожидании и что то пишет на диск – xellan Jun 27 '23 at 16:50

1 Answers1

1

Может кому пригодится, сохранение нескольких файлов в потоковой передаче, также можно чередавать с json запрос и т.п. Статья на данную тему https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-7.0

Код контролера

public class FileController : Controller
    {
        const long _RequestSizeLimit = (long)1e+10;
        [HttpPost("/uploadfile")]
        [RequestSizeLimit(_RequestSizeLimit)]
        [RequestFormLimits(MultipartBodyLengthLimit = _RequestSizeLimit)]
        public async Task<IActionResult> UploadPhysical()
        {
            if (!IsMultipartContentType(Request.ContentType))
                return BadRequest("File\",\r\n\"The request couldn't be processed (Error 1).");
        var boundary = GetBoundary(Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(Request.ContentType));
        var reader = new MultipartReader(boundary, HttpContext.Request.Body);
        var section = await reader.ReadNextSectionAsync();

        while (section != null)
        {
            var hasContentDispositionHeader = System.Net.Http.Headers.ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
            if (hasContentDispositionHeader)
            {
                if (HasFileContentDisposition(contentDisposition))
                    await ProcessStreamedFile(section, contentDisposition);
                if (HasJsonContentDisposition(contentDisposition, section))
                { 
                    var json = await section.ReadAsStringAsync();
                    var example = await JsonSerializer.DeserializeAsync&lt;MyObject&gt;(json);
                }
            }
            section = await reader.ReadNextSectionAsync();
        }
        return Ok();
    }
    string GetBoundary(Microsoft.Net.Http.Headers.MediaTypeHeaderValue contentType) =&gt; HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
    bool IsMultipartContentType(string contentType) =&gt; !string.IsNullOrEmpty(contentType) &amp;&amp; contentType.IndexOf(&quot;multipart/&quot;, StringComparison.OrdinalIgnoreCase) &gt;= 0;
    /// &lt;summary&gt;
    /// Проверка на наличие файла в потоке
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;contentDisposition&quot;&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    bool HasFileContentDisposition(System.Net.Http.Headers.ContentDispositionHeaderValue contentDisposition)
    {
        // Content-Disposition: form-data; name=&quot;myfile1&quot;; filename=&quot;Misc 002.jpg&quot;
        return contentDisposition != null
            &amp;&amp; contentDisposition.DispositionType.Equals(&quot;form-data&quot;)
            &amp;&amp; (!string.IsNullOrEmpty(contentDisposition.FileName)
                || !string.IsNullOrEmpty(contentDisposition.FileNameStar));
    }
    /// &lt;summary&gt;
    /// Проверка является на Json
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;contentDisposition&quot;&gt;&lt;/param&gt;
    /// &lt;param name=&quot;section&quot;&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    bool HasJsonContentDisposition(System.Net.Http.Headers.ContentDispositionHeaderValue contentDisposition, MultipartSection section)
    {
        return contentDisposition != null &amp;&amp; contentDisposition.DispositionType.Equals(&quot;form-data&quot;) 
            &amp;&amp; section.ContentType != null
            &amp;&amp; section.ContentType.Contains(&quot;application/json&quot;);
    }
    /// &lt;summary&gt;
    /// Запись файла с удалением запрещенных символов из пути, в contentDisposition.FileName могут присутствовать кавычки 
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;section&quot;&gt;&lt;/param&gt;
    /// &lt;param name=&quot;contentDisposition&quot;&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    async Task ProcessStreamedFile(MultipartSection section, System.Net.Http.Headers.ContentDispositionHeaderValue contentDisposition)
    {
        string fileName = contentDisposition.FileName;
        string invalidChars = new string(Path.GetInvalidFileNameChars());
        fileName = new string(fileName.Where(c =&gt; !invalidChars.Contains(c)).ToArray());
        using (var memoryStream = new FileStream(fileName, FileMode.Create))
            await section.Body.CopyToAsync(memoryStream, 1024 * 50);
    }
}

Код для отправки данных на стороне клиента

public async Task SendFiles(List<string> filePaths, string url)
        {
            using (HttpClient client = new HttpClient())
            {
                client.Timeout = TimeSpan.FromMinutes(10);
                client.DefaultRequestHeaders.Add("Token", "1234");
            // Создаем объект MultipartFormDataContent для передачи файлов
            using (MultipartFormDataContent content = new MultipartFormDataContent())
            {
                var json = new StringContent(&quot;\&quot;key\&quot;: \&quot;Test\&quot;&quot;, Encoding.UTF8, &quot;application/json&quot;);
                content.Add(json, &quot;\&quot;json\&quot;&quot;);
                foreach (var filePath in filePaths)
                {
                    var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
                    var fileContent = new StreamContent(fileStream);
                    content.Add(fileContent, &quot;files&quot;, System.IO.Path.GetFileName(filePath));
                }

                // Отправляем потоковый запрос на сервер с помощью метода HTTP POST
                HttpResponseMessage response = await client.PostAsync(url, content);

                // Получаем ответ от сервера и проверяем его статусный код
                if (response.IsSuccessStatusCode)
                {
                    Console.WriteLine($&quot;{filePaths.Count} files have been sent successfully.&quot;);
                }
                else
                {
                    Console.WriteLine(&quot;An error occurred while sending files.&quot;);
                }
            }
        }
    }

xellan
  • 560
  • 3
  • 11