Bad performance when offering files to download with HttpListener

前端 未结 1 1640
陌清茗
陌清茗 2021-01-07 10:58

I\'m trying to create a simple web server using HttpListener in C# and offer files to download. I\'m seeing really bad transfer rates especially co

相关标签:
1条回答
  • 2021-01-07 11:16

    Overall

    The two tests that were run (C# HttpListener serving a file and an smb file copy test) contain too many variables to draw any useful conclusions about the performance of HttpListener vs. native code.

    All other code in such cases should be suspect of causing the performance problem and should be removed from the test case.

    Unfortunately, the question's implementation of serving a file is non-optimal as it reads a chunk from the file into a managed byte array, then blocks on a call to write that block to the kernel. Its copying the bytes of the file into a managed array and back out of a managed array (adding no value in the process). With .Net 4.5 you could have called CopyToAsync between the file stream and the output stream, which would get you out of the business of figuring out how to do that in parallel.

    Conclusions

    The test below shows that HttpListener is just as fast at sending back bytes as IIS Express 8.0 returning a file. For this post I tested this on a cheesy 802.11n network with the servers on a VM and still reached 100+ Mbps with both HttpListener and IIS Express.

    The only thing that needs to be changed in the original post is how it reads the file to relay it back to the client.

    If you want to serve files via HTTP you should probably use an existing web server that handles both the HTTP side of things and the file opening/caching/relaying. You will find it hard to beat existing web servers, particularly when you bring gzipping dynamic responses into the picture (in that case the naive approach gzips the entire response, accidentally, before sending any of it, which wastes time that could have been used for sending bytes).

    Better Test to Isolate Just HttpListener's Performance

    I created a test that returns a 10 MB string of integers (generated once on startup) to allow testing the speed at which HttpListener can return data when it's given the whole block up front (which is similar to what it could do when you use CopyToAsync).

    Test Setup

    Client Computer: MacBook Air mid-2013, 1.7 GHz Core i7 Server Computer: iMac mid-2011, 3.4 GHz Core i7 - Windows 8.1 Hosted in VMWare Fusion 6.0, Bridged Networking Network: 802.11n via Airport Extreme (located 8 feet away) Download Client: curl on Mac OS X

    Test Results

    IIS Express 8.0 was configured to serve an 18 MB file and the HttpListenerSpeed program was setup to return 10 MB and 100 MB responses. The test results were essentially identical.

    IIS Express 8.0 Results

    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;
                }
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题