Плохая производительность при предложении файлов для загрузки с HttpListener

Я пытаюсь создать простой веб-сервер, используяHttpListener вC# и предлагать файлы для скачивания. Я вижу очень плохую скорость передачи данных, особенно по сравнению с копированием того же файла из общего ресурса. Это известноHttpListener и что можно сделать, чтобы улучшить его?

Вот некоторая дополнительная информация об исследованиях, которые я провел по этой проблеме. Скорость загрузки значительно улучшается при локальном подключении, но в этом случае копирование файла выполняется практически мгновенно, поэтому сложно измерить разницу. При удаленном подключении LAN окружающая среда, машины находятся рядом друг с другом) однако время передачи примерно в 25 раз больше времени простой копии файла из общего ресурса. Кажется, доступная пропускная способность сети не используется для ускорения этого процесса.

Я нашел несколько других вопросов и обсуждений оHttpListener которые указывают на похожие проблемы, см. здесь:

HttpListener против собственной производительности

HttpListener Оптимизация производительности (это не касается загрузок)

MSDN документы также утверждают, чтоHttpListener основан наhttp.sys который позволяет регулировать пропускную способность. Может быть, здесь происходит какое-то нежелательное ограничение полосы пропускания или что-то не так с моим кодом? На машинах, на которых я тестировал (Windows 7 и Windows 2008 R2), IIS отсутствовал.

В моем примере я начинаюHttpListener вот так

<code>  HttpListener listener = new HttpListener();
  listener.Prefixes.Add("http://*:80/");
  listener.Start();
</code>

Вот код для моей простой загрузки файла:

<code>  HttpListenerResponse response = null;
  try {
      HttpListenerContext context = listener.GetContext();

      response = context.Response;

      using( FileStream fs = File.OpenRead( @"c:\downloadsample\testfile.pdf" ) ) {

          byte[] buffer = new byte[ 32768 ];
          int read;
          while( ( read = fs.Read( buffer, 0, buffer.Length ) ) > 0 ) {
              response.OutputStream.Write( buffer, 0, read );
          }
      }

  } finally {
      if( response != null )
          response.Close();
  }
</code>

(редактировать: исправлены некоторые ссылки ...)

 lichtalberich08 мая 2012 г., 10:08
Отвечая на себя, все зависит от правильных заголовков и особенно размера буфера. В моей локальной сети я могу получить скорости, близкие к обычной копии файла, например, используя Размер буфера 1 МБ (1048576). Могу поспорить, что ваш намек на использование асинхронного ввода-вывода ускорит процесс еще больше. Большое спасибо за вашу помощь
 Alois Kraus07 мая 2012 г., 19:34
Что Стив означает, что пока вы читаете следующий блок, вы тем временем уже можете отправить ранее прочитанный фрагмент по сети. В настоящее время вы 2 раза заблокированы IO. Один раз, прочитав с диска, и второй раз, когда вы отправите его по проводам. Вполне возможно, что у вас высокое время GC. Проверьте с помощью профилировщика, где большую часть времени проводят.
 lichtalberich07 мая 2012 г., 20:17
Стив и Алоис, спасибо за ваши комментарии. Я также тестировал с клиентом C # (HttpWebRequest) и имел асинхронный ввод / вывод для записи полученных данных. Честно говоря, я сомневаюсь, что изменение этого сделает его быстрее на порядок. Я (а) поменяю его на асинхронную отправку и (б) свяжусь с профилировщиком и попытаюсь выяснить, что он делает.
 Steve Townsend07 мая 2012 г., 19:00
Вам, вероятно, нужно использовать асинхронный ввод-вывод, чтобы сделать это быстрее

Ответы на вопрос(1)

В цело

обслуживающий файл и тест копирования файла smb) содержат слишком много переменных, чтобы сделать какие-либо полезные выводы о производительности HttpListener по сравнению с собственным кодом.

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

К сожалению, реализация вопроса об обслуживании файла неоптимальна, поскольку она считывает кусок из файла в управляемый байтовый массив, а затем блокирует вызов для записи этого блока в ядро. Это копирование байтов файла в управляемый массив и обратно из управляемого массива (не добавляя никакого значения в процессе). В .Net 4.5 вы могли бы вызывать CopyToAsync между файловым потоком и выходным потоком, что избавило бы вас от необходимости выяснять, как сделать это параллельно.

Выводы

Тест ниже показывает, что HttpListener так же быстро отправляет байты, как IIS Express 8.0, возвращая файл. В этом посте я проверил это в нездоровой сети 802.11n с серверами на виртуальной машине и по-прежнему достигал скорости 100+ Мбит / с как с HttpListener, так и с IIS Expres

Единственное, что нужно изменить в исходном сообщении, - это как он читает файл, чтобы передать его обратно клиенту.

Если вы хотите передавать файлы через HTTP, вам, вероятно, следует использовать существующий веб-сервер, который обрабатывает как HTTP-часть, так и открытие / кэширование / ретрансляцию файлов. Вам будет трудно превзойти существующие веб-серверы, особенно когда вы добавляете динамические ответы gzip на картинку (в этом случае наивный подход случайным образом gzips весь ответ перед отправкой любого из них, что тратит время, которое могло бы быть использовано для отправка байтов).

Лучший тест для изоляции производительности HttpListener

Я создал тест, который возвращает строку целых чисел размером 10 МБ (сгенерированную один раз при запуске), чтобы позволить проверить скорость, с которой HttpListener может возвращать данные, когда ему передается весь блок заранее (что аналогично тому, что он мог делать при использовании). CopyToAsync).

Испытательная установк

Client Computer: MacBook Air, середина 2013 года, сервер Core i7 с тактовой частотой 1,7 ГГц, компьютер: iMac, середина 2011 года, Core i7 с частотой 3,4 ГГц, Windows 8.1, размещенный в VMWare Fusion 6.0, мостовая сеть: 802.11n через Airport Extreme (расположен в 8 футах) Скачать клиент: curl на Mac OS X

Результаты тест

IIS Express 8.0 был настроен для обслуживания файла размером 18 МБ, а программа HttpListenerSpeed была настроена на получение ответов 10 МБ и 100 МБ. Результаты теста были практически идентичны.

езультаты @IIS Express 8.0
Harolds-MacBook-Air:~ huntharo$ curl -L http://iMacWin81.local:8000/TortoiseSVN-1.8.2.24708-x64-svn-1.8.3.msi > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 18.4M  100 18.4M    0     0  13.1M      0  0:00:01  0:00:01 --:--:-- 13.1M

Harolds-MacBook-Air:~ huntharo$ curl -L http://iMacWin81.local:8000/TortoiseSVN-1.8.2.24708-x64-svn-1.8.3.msi > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 18.4M  100 18.4M    0     0  13.0M      0  0:00:01  0:00:01 --:--:-- 13.1M

Harolds-MacBook-Air:~ huntharo$ curl -L http://iMacWin81.local:8000/TortoiseSVN-1.8.2.24708-x64-svn-1.8.3.msi > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 18.4M  100 18.4M    0     0  9688k      0  0:00:01  0:00:01 --:--:-- 9737k
HttpListenerSpeed Results
Harolds-MacBook-Air:~ huntharo$ curl -L http://iMacWin81.local:8080/garbage > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 18.4M  100 18.4M    0     0  12.6M      0  0:00:01  0:00:01 --:--:-- 13.1M

Harolds-MacBook-Air:~ huntharo$ curl -L http://iMacWin81.local:8080/garbage > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 18.4M  100 18.4M    0     0  13.1M      0  0:00:01  0:00:01 --:--:-- 13.1M

Harolds-MacBook-Air:~ huntharo$ curl -L http://iMacWin81.local:8080/garbage > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 18.4M  100 18.4M    0     0  13.2M      0  0:00:01  0:00:01 --:--:-- 13.2M
HttpListenerSpeed Code
using System;
using System.Threading.Tasks;
using System.Net;
using System.Threading;

namespace HttpListenerSpeed
{
    class Program
    {
        static void Main(string[] args)
        {
            var listener = new Listener();

            Console.WriteLine("Press Enter to exit");
            Console.ReadLine();

            listener.Shutdown();
        }
    }

    internal class Listener
    {
        private const int RequestDispatchThreadCount = 4;
        private readonly HttpListener _httpListener = new HttpListener();
        private readonly Thread[] _requestThreads;
        private readonly byte[] _garbage;

        internal Listener()
        {
            _garbage = CreateGarbage();

            _httpListener.Prefixes.Add("http://*:8080/");
            _httpListener.Start();
            _requestThreads = new Thread[RequestDispatchThreadCount];
            for (int i = 0; i < _requestThreads.Length; i++)
            {
                _requestThreads[i] = new Thread(RequestDispatchThread);
                _requestThreads[i].Start();
            }
        }

        private static byte[] CreateGarbage()
        {
            int[] numbers = new int[2150000];

            for (int i = 0; i < numbers.Length; i++)
            {
                numbers[i] = 1000000 + i;
            }

            Shuffle(numbers);

            return System.Text.Encoding.UTF8.GetBytes(string.Join<int>(", ", numbers));
        }

        private static void Shuffle<T>(T[] array)
        {
            Random random = new Random();
            for (int i = array.Length; i > 1; i--)
            {
                // Pick random element to swap.
                int j = random.Next(i); // 0 <= j <= i-1
                // Swap.
                T tmp = array[j];
                array[j] = array[i - 1];
                array[i - 1] = tmp;
            }
        }

        private void RequestDispatchThread()
        {
            while (_httpListener.IsListening)
            {
                string url = string.Empty;

                try
                {
                    // Yeah, this blocks, but that's the whole point of this thread
                    // Note: the number of threads that are dispatching requets in no way limits the number of "open" requests that we can have
                    var context = _httpListener.GetContext();

                    // For this demo we only support GET
                    if (context.Request.HttpMethod != "GET")
                    {
                        context.Response.StatusCode = (int)HttpStatusCode.NotFound;
                        context.Response.Close();
                    }

                    // Don't care what the URL is... you're getting a bunch of garbage, and you better like it!
                    context.Response.StatusCode = (int)HttpStatusCode.OK;
                    context.Response.ContentLength64 = _garbage.Length;
                    context.Response.OutputStream.BeginWrite(_garbage, 0, _garbage.Length, result =>
                    {
                        context.Response.OutputStream.EndWrite(result);
                        context.Response.Close();
                    }, context);
                }
                catch (System.Net.HttpListenerException e)
                {
                    // Bail out - this happens on shutdown
                    return;
                }
                catch (Exception e)
                {
                    Console.WriteLine("Unexpected exception: {0}", e.Message);
                }
            }
        }

        internal void Shutdown()
        {
            if (!_httpListener.IsListening)
            {
                return;
            }

            // Stop the listener
            _httpListener.Stop();

            //  Wait for all the request threads to stop
            for (int i = 0; i < _requestThreads.Length; i++)
            {
                var thread = _requestThreads[i];
                if (thread != null) thread.Join();
                _requestThreads[i] = null;
            }
        }
    }
}
 GBrian21 янв. 2015 г., 05:49
Привет, Хантхаро, я ожидалBeginContext создать отдельный поток для обработки запроса, но из этого Blog.stephencleary.com / 2013/11 / там-нет-нет-thread.html кажется, это неправда, поэтому я буду следовать вашему подходу. Благодарность
 GBrian18 янв. 2015 г., 16:37
"// Примечание: количество потоков, отправляющих запросы, никоим образом не ограничивает количество" открытых "запросов, которые мы можем иметь". Извините, не вижу этого. Каждый поток блокируется до начала записи, поэтому только 4 запроса могут быть обработаны одновременно. В случае, если генерация всех запросов занимает больше времени, новые блоки будут заблокированы. Я ошибся? Как насчет получения контекста асинхронно?
 huntharo18 янв. 2015 г., 19:54
Он использует асинхронный шаблон, но использует более старый стиль начала / конца, чтобы больше людей могли его использовать. Я могу заверить вас, что вы можете отправить более 4 запросов одновременно, и все они будут забраны. Вы увидите странные результаты, если вы запустите тестовый клиент и тестовый сервер на одной машине и у вас будет менее 4 процессорных ядер. В идеале вы хотели бы проверить это с 8 или более ядрами и двумя машинами. Тогда вы наверняка увидите более 4 запросов одновременно.

Ваш ответ на вопрос