Mauvaises performances lors de l’offre de fichiers à télécharger avec HttpListener

J’essaie de créer un serveur Web simple à l’aide de HttpListener en C# et de proposer des fichiers à télécharger. Je constate de très mauvais taux de transfert, en particulier par rapport à la copie du même fichier à partir d’un partage. Est-ce connu de HttpListener et que peut-on faire pour l’améliorer?

Voici quelques informations supplémentaires sur la recherche que j’ai faite sur le problème. Les taux de téléchargement s’améliorent considérablement lors de la connexion locale, mais la copie du fichier s’effectue presque instantanément dans ce cas, il est donc difficile de mesurer un taux de différence. Lors de la connexion à distance (environnement LAN , machines proches les unes des autres), le temps de transfert est environ 25 fois supérieur à celui d’une copie de fichier simple à partir d’un partage. La bande passante réseau disponible ne semble pas être utilisée pour accélérer le processus.

J’ai trouvé d’autres questions et discussions sur HttpListener qui semblent indiquer des problèmes similaires, voir ici:

HttpListener vs performance native

Optimisation des performances de HttpListener (ceci ne concerne toutefois pas les téléchargements)

Les documents MSDN indiquent également que HttpListener est basé sur http.sys ce qui permet une limitation de la bande passante. Se pourrait-il qu’une limitation indésirable de la bande passante ait lieu ici ou y a-t-il un problème avec mon code? Sur les machines avec lesquelles j’ai testé (Windows 7 et Windows 2008 R2), aucun IIS n’était présent.

Dans mon exemple, je commence un HttpListener comme ceci:

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

Voici le code pour mon téléchargement de fichier simple:

  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(); } 

(edit: correction de quelques liens …)

Global

Les deux tests exécutés (C # HttpListener servant un fichier et un test de copie de fichier smb) contiennent trop de variables pour tirer des conclusions utiles sur les performances de HttpListener par rapport au code natif.

Dans ce cas, tous les autres codes doivent être suspectés d’être à l’origine du problème de performances et doivent être supprimés du scénario de test.

Malheureusement, la mise en œuvre de la question consistant à servir un fichier n’est pas optimale car elle lit une partie du fichier dans un tableau d’octets géré, puis se bloque lors d’un appel pour écrire ce bloc dans le kernel. Sa copie des octets du fichier dans un tableau géré et en arrière d’un tableau géré (n’ajoutant aucune valeur au processus). Avec .Net 4.5, vous auriez pu appeler CopyToAsync entre le stream de fichiers et le stream de sortie, ce qui vous éviterait de chercher comment faire cela en parallèle.

Conclusions

Le test ci-dessous montre que HttpListener est tout aussi rapide à renvoyer des octets que IIS Express 8.0 renvoyant un fichier. Pour cet article, j’ai testé cela sur un réseau ringard 802.11n avec les serveurs d’une machine virtuelle et j’ai toujours atteint plus de 100 Mbps avec HttpListener et IIS Express.

La seule chose à modifier dans le message d’origine est la façon dont il lit le fichier pour le relayer au client.

Si vous voulez servir des fichiers via HTTP, vous devriez probablement utiliser un serveur Web existant qui gère à la fois le côté HTTP des choses et l’ouverture / la mise en cache / le relais des fichiers. Vous aurez du mal à battre les serveurs Web existants, en particulier lorsque vous intégrez des réponses dynamics gzipping dans l’image (dans ce cas, l’approche naïve gzipote la réponse dans son intégralité, accidentellement, avant de l’envoyer, ce qui représente une perte de temps qui aurait pu être utilisée envoi d’octets).

Un meilleur test pour isoler les performances de HttpListener

J’ai créé un test qui renvoie une chaîne d’entiers de 10 Mo (générée une fois au démarrage) pour permettre de tester la vitesse à laquelle HttpListener peut renvoyer des données lorsqu’il reçoit l’intégralité du bloc (ce qui est similaire à ce qu’il pourrait faire lorsque vous utilisez CopyToAsync. ).

Configuration de test

Ordinateur client: MacBook Air mi-2013, serveur Core i7 à 1,7 GHz. Ordinateur: iMac mi-2011, Core i7 à 3,4 GHz – Windows 8.1. Hébergé sur VMWare Fusion 6.0, réseau ponté: 802.11n via Airport Extreme (situé à 8 pieds). Télécharger Client: curl sur Mac OS X

Résultats de test

IIS Express 8.0 a été configuré pour servir un fichier de 18 Mo et le programme HttpListenerSpeed ​​a été configuré pour renvoyer des réponses de 10 et 100 Mo. Les résultats du test étaient essentiellement identiques.

Résultats 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 

Résultats HttpListenerSpeed

 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(ssortingng[] 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(", ", numbers)); } private static void Shuffle(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; } } } }