如何在ASP.NET Response中传递大文件?
我不是从数据库中寻找任何stream式文件内容的替代品,事实上,我正在寻找问题的根源,这是运行文件,直到IIS 6,我们在经典模式下运行我们的应用程序,现在我们升级我们的IIS到7,我们在pipe道模式下运行应用程序池,并开始此问题。
我有一个处理程序,我必须提供大文件到客户端请求。 我面临以下问题,
文件的平均大小为4到100 MB,所以我们考虑80MB的文件下载情况。
缓冲,慢启动
Response.BufferOutput = True;
这导致文件的启动非常缓慢,因为用户下载甚至进度条直到几秒钟才出现,通常是3到20秒,原因在于IIS首先读取整个文件,确定内容长度然后开始文件传输。 文件正在video播放器中播放,而且运行速度非常慢,但是iPad只会首先下载文件的一部分,所以它的运行速度很快。
缓冲closures,无内容长度,快速启动,无进展
Reponse.BufferOutput = False;
这导致立即开始,但最终客户端(典型的浏览器,如Chrome)不知道内容长度,因为IIS不知道,所以它不显示进度,而是说,X KB下载。
缓冲closures,手动内容长度,快速启动,进度和协议违例
Response.BufferOutput = False; Response.AddHeader("Content-Length", file.Length);
这导致在Chrome等立即文件下载正确,但在某些情况下,IIS处理程序导致“远程客户端closures连接”错误(这是非常频繁的)和其他WebClient导致违反协议。 这会发生所有请求的5%到10%,而不是每个请求。
我猜是什么事情发生,IIS不会发送任何称为100继续,当我们不做缓冲和客户端可能断开不期望任何输出。 但是,从源文件读取文件可能需要更长的时间,但在客户端,我增加了超时但看起来像IIS超时,并没有控制。
有没有反正我可以强制响应发送100继续,不让任何人closures连接?
UPDATE
我在Firefox / Chrome中发现了以下头文件,在协议违例或错误头文件中没有什么不寻常的地方。
Access-Control-Allow-Headers:* Access-Control-Allow-Methods:POST, GET, OPTIONS Access-Control-Allow-Origin:* Access-Control-Max-Age:1728000 Cache-Control:private Content-Disposition:attachment; filename="24.jpg" Content-Length:22355 Content-Type:image/pjpeg Date:Wed, 07 Mar 2012 13:40:26 GMT Server:Microsoft-IIS/7.5 X-AspNet-Version:4.0.30319 X-Powered-By:ASP.NET
更新2
转向回收仍然没有提供太多,但我已经增加了我的MaxWorkerProcess到8,现在我得到的错误数量较less。
但平均而言,在一秒钟内有200个请求中,有2到10个请求失败。这种情况几乎每隔一秒钟都会发生。
更新3
继续5%的请求失败,“服务器提交违反协议。部分= ResponseStatusLine”,我有另一个程序,从Web服务器使用WebClient下载内容,每秒4-5次,平均我有5%的请求失败。 无论如何追踪WebClient失败?
问题重新定义
收到零字节文件
IIS由于某种原因closures了连接,在WebConfig的客户端,我收到了不是零字节的文件的0字节,我们做了SHA1哈希检查,这告诉我们在IIS web服务器中没有logging错误。
这是我的错误,它解决了,因为我们正在使用entity framework,它正在阅读脏(未提交的行)作为读取不在事务范围,把它在事务范围已经解决了这个问题。
协议违反例外提出
WebClient抛出WebException说:“服务器提交协议冲突。部分= ResponseStatusLine。
我知道我可以启用不安全的头parsing,但这不是重点,当它是我的HTTP处理程序发送正确的头,不知道为什么IIS发送额外的任何东西(检查在Firefox和铬,没有什么不寻常的),这种情况只发生2次数百分比。
更新4
发现sc-win32 64错误,我在某处读取WebLimits MinBytesPerSecond必须从240更改为0,仍然是一切。 但是我注意到,每当IISlogging64 sc-win32错误,IISloggingHTTP状态为200,但有一些错误。 现在我无法打开200失败跟踪logging,因为它会导致大量的文件。
通过增加MinBytesPerSecond和禁用会话解决了上述两个问题,我添加了总结每个点的详细答案。
虽然正确的方式来提供IIS中的大文件是以下选项,
- 在WebLimits中设置MinBytesPerSecond为零(这肯定会有助于提高性能,因为IISselectclosures使用较小尺寸传输的KeepAlive连接的客户端)
- 分配更多的工作进程到应用程序池,我已经设置为8,现在只有当您的服务器正在分发更大的文件,这应该完成。 这肯定会导致其他网站执行速度较慢,但是这将确保更好的交付。 我们已经设置为8,因为这台服务器只有一个网站,它只是提供巨大的文件。
- closures应用程序池回收
- closures会话
- 保持缓冲
- 在每个以下步骤之前,检查Response.IsClientConnected是否为真,否则放弃,不发送任何东西。
- 在发送文件之前设置Content-Length
- 刷新响应
- 写入输出stream,并定期刷新
当您将bufferOutput的内容长度设置为false时,失败的可能原因是IIS尝试gzip发送的文件,通过设置Content-Length IIS不能将其更改回压缩的内容,错误开始(*)。
因此, keep the BufferOutput to false, and second disable the gzip from iis for the files you send
的iis gzip,或者禁用所有文件的iis gzip,并以编程方式处理gzip部分,从而避免gzip发送的文件。
一些类似的问题,出于同样的原因: 在负载平衡的服务器上加载时,ASP.NET站点有时会冻结和/或在页面顶部显示奇文本
HTTP压缩:一些外部脚本/ CSS在某些时候不能正确解压缩
(*)为什么不再改变它? 因为从设置标题的那一刻起,你就不能参与其中,除非你在IIS上启用了这个选项,并且假设标题还没有准备好发送给浏览器。
跟进
如果没有gziped,我想到的下一件事是文件被发送,由于某种原因连接被延迟,并得到超时和closures。 所以你得到“远程主机closures连接”。
这可以根据原因来解决:
- 客户真的closures了连接
- 超时是从页面本身,如果您使用处理程序(再次,可能,该消息必须是“页面超时”)。
- 超时来自空闲等待,页面占用超过执行时间,超时并closures连接。 也许在这种情况下,消息是页面超时。
- 池在发送文件的那一刻进行回收 。 禁用所有池回收! 这是我现在可以想到的最可能的情况。
如果它来自IIS,请转至网站属性,并确保设置最大的“连接超时”和“启用HTTP保持活动”。
通过更改web.config页面超时(您可以通过编程方式更改一个特定的页面)
<httpRuntime executionTimeout="43200"
也看看: http : //weblogs.asp.net/aghausman/archive/2009/02/20/prevent-request-timeout-in-asp-net.aspx
会话locking
还有一件事需要检查,就是不要在用于发送文件的处理程序上使用会话,因为会话会locking动作,直到完成,如果用户花费较长时间下载文件,则可能会得到第二个时间到。
一些亲戚:
调用aspx页面随机缓慢返回图像
完全取代ASP.Net的会话
Response.WriteFile函数失败,并提供504网关超时
我会做的是使用不那么知名的ASP.NET Response.TransmitFile方法,因为它非常快速(可能使用IIS内核caching)并照顾所有标题的东西。 它基于Windows非托pipe的TransmitFile API。
但是为了能够使用这个API,你需要一个物理文件来传输。 所以这里是一个伪c#代码,解释如何用虚构的myCacheFilePath物理文件path来做到这一点。 它也支持客户端caching的可能性。 当然,如果你已经有一个文件,你不需要创buildcaching:
if (!File.Exists(myCacheFilePath)) { LoadMyCache(...); // saves the file to disk. don't do this if your source is already a physical file (not stored in a db for example). } // we suppose user-agent (browser) cache is enabled // check appropriate If-Modified-Since header DateTime ifModifiedSince = DateTime.MaxValue; string ifm = context.Request.Headers["If-Modified-Since"]; if (!string.IsNullOrEmpty(ifm)) { try { ifModifiedSince = DateTime.Parse(ifm, DateTimeFormatInfo.InvariantInfo); } catch { // do nothing } // file has not changed, just send this information but truncate milliseconds if (ifModifiedSince == TruncateMilliseconds(File.GetLastWriteTime(myCacheFilePath))) { ResponseWriteNotModified(...); // HTTP 304 return; } } Response.ContentType = contentType; // set your file content type here Response.AddHeader("Last-Modified", File.GetLastWriteTimeUtc(myCacheFilePath).ToString("r", DateTimeFormatInfo.InvariantInfo)); // tell the client to cache that file // this API uses windows lower levels directly and is not memory/cpu intensive on Windows platform to send one file. It also caches files in the kernel. Response.TransmitFile(myCacheFilePath)
这段代码适合我。 它立即启动数据stream到客户端。 它显示下载过程中的进展。 它不违反HTTP。 内容长度标题被指定并且不使用被分组的传输编码。
protected void PrepareResponseStream(string clientFileName, HttpContext context, long sourceStreamLength) { context.Response.ClearHeaders(); context.Response.Clear(); context.Response.ContentType = "application/pdf"; context.Response.AddHeader("Content-Disposition", string.Format("filename=\"{0}\"", clientFileName)); //set cachebility to private to allow IE to download it via HTTPS. Otherwise it might refuse it //see reason for HttpCacheability.Private at http://support.microsoft.com/kb/812935 context.Response.Cache.SetCacheability(HttpCacheability.Private); context.Response.Buffer = false; context.Response.BufferOutput = false; context.Response.AddHeader("Content-Length", sourceStreamLength.ToString (System.Globalization.CultureInfo.InvariantCulture)); } protected void WriteDataToOutputStream(Stream sourceStream, long sourceStreamLength, string clientFileName, HttpContext context) { PrepareResponseStream(clientFileName, context, sourceStreamLength); const int BlockSize = 4 * 1024 * 1024; byte[] buffer = new byte[BlockSize]; int bytesRead; Stream outStream = m_Context.Response.OutputStream; while ((bytesRead = sourceStream.Read(buffer, 0, BlockSize)) > 0) { outStream.Write(buffer, 0, bytesRead); } outStream.Flush(); }