使用HttpListener提供下载文件时性能不佳

我正在尝试使用C# HttpListener创建一个简单的Web服务器,并提供要下载的文件。 我发现传输速率非常差,特别是与从共享中复制相同文件相比。 这是否为HttpListener所知,可以采取哪些措施来改进它?

这里有一些关于我对这个问题所做研究的其他信息。 在本地连接时下载速率提高了很多,但在这种情况下几乎立即复制文件,因此很难测量差异比率。 然而,当远程连接( LAN环境,彼此相邻的机器)时,传输时间大约是从共享进行简单文件复制的时间的25倍。 似乎没有使用可用的网络带宽来加快速度。

我发现了一些关于HttpListener其他问题和讨论似乎表明了类似的问题,请看这里:

HttpListener vs本机性能

HttpListener性能优化 (但这不是关于下载)

MSDN文档还声明HttpListener基于http.sys ,允许带宽限制。 可能是这里发生了一些不必要的带宽限制或我的代码有问题吗? 在我测试过的机器上(Windows 7和Windows 2008 R2),没有IIS存在。

在我的示例中,我正在启动一个HttpListener如下所示:

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

这是我简单文件下载的代码:

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

(编辑:修复了一些链接…)

总体

运行的两个测试(C#HttpListener提供文件和smb文件复制测试)包含太多变量,无法得出有关HttpListener与本机代码性能的任何有用结论。

在这种情况下,所有其他代码都应该怀疑导致性能问题,应该从测试用例中删除。

不幸的是,问题的服务文件的实现是非最佳的,因为它从文件中读取一个块到一个托管字节数组,然后在调用时阻塞将该块写入内核。 它将文件的字节复制到托管数组中并从托管数组中退出(在进程中不添加任何值)。 使用.Net 4.5,您可以在文件流和输出流之间调用CopyToAsync,这将使您无法找到并行执行此操作的方法。

结论

下面的测试显示HttpListener在返回文件时发送回字节的速度与IIS Express 8.0返回文件一样快。 对于这篇文章,我在虚拟的802.11n网络上测试了这一点,其中VM上的服务器仍然使用HttpListener和IIS Express达到100+ Mbps。

在原始post中唯一需要更改的是它如何读取文件以将其中继回客户端。

如果您想通过HTTP提供文件,您应该使用现有的Web服务器来处理事物的HTTP端和文件打开/缓存/中继。 您会发现很难击败现有的Web服务器,特别是当您将gzipping动态响应带入图片时(在这种情况下,天真的方法会在发送任何响应之前意外地gzips整个响应,这浪费了可能用于的时间发送字节)。

更好的测试来隔离HttpListener的性能

我创建了一个测试,返回一个10 MB的整数字符串(在启动时生成一次),以便测试HttpListener在预先给出整个块时返回数据的速度(这与使用CopyToAsync时的操作类似) )。

测试设置

客户端计算机:MacBook Air 2013年中期,1.7 GHz Core i7服务器计算机:iMac 2011年中期,3.4 GHz Core i7 – Windows 8.1托管在VMWare Fusion 6.0,桥接网络:802.11n via Airport Extreme(位于8英尺外)下载客户端:在Mac OS X上curl

检测结果

IIS Express 8.0配置为提供18 MB文件,HttpListenerSpeed程序设置为返回10 MB和100 MB响应。 测试结果基本相同。

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结果

 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代码

 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(", ", 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; } } } }