Dime 流式上传






4.73/5 (37投票s)
2005年3月10日
9分钟阅读

504414

2872
本文概述了一种克服通过 Web 服务发送大文件问题的技术。
通知:有新文章可用
本文使用了 Web 服务增强功能 (WSE) 版本 2 和 .NET 版本 1.1。我写了一篇使用 WSE 3 和 .NET 2.0 的新文章,使用了 MTOM 而非 DIME,后者更快且更易于使用。本文保留给需要 .NET 1.1 / WSE 2 解决方案的用户。
摘要
本文介绍了一种解决方案,用于通过 HTTP Web 服务上传大文件,克服 IIS 请求超时和 IIS/ASP.NET 请求大小限制。除了提供大文件的可靠传输系统外,它还可以使 Windows Forms 应用程序使用进度条定期显示传输状态。
引言
如果您曾开发过将文件(可能很大)发送到具有 HTTP Web 服务的 Web 服务器的 Windows Forms 应用程序,您可能已经知道 .NET 框架处理此任务的效果不佳。Web 服务最初设计用于发送单个消息,但我们开发者就像我们一样,开始使用它们发送字节数组,即文件的二进制内容。对于大文件,这种方法不可扩展,因为 IIS 有请求超时时间和最大请求大小,这限制了我们可以发送到服务器的任何 Web 服务请求的大小。您还可能让用户等待很长时间才能返回操作成功或失败的结果。您也可能在客户端应用程序或服务器上遇到 OutOfMemoryException
(因为字节数组必须一次性存储在内存中)。
感谢 Tulio Rodriguez
本文最初概述了一种将文件分割成 X 个文件片段,然后将它们全部发送到服务器,并在服务器上合并以重现原始文件的解决方案。这是一种通过 HTTP 实现“分块”并为用户提供定期反馈的快速方法(前提是分块足够小)。这种方法的问题显然是在发生任何意外错误时,客户机和服务器计算机上存在孤立文件的风险。
本文已根据 Tulio Rodriguez 允许我使用和发布的代码进行了更新。他的方法不是将文件分割成片段,而是将字节数组一次发送一个到服务器,服务器只是将每个块追加到服务器上的文件中。这是一个更优雅的解决方案,他为此方法付出了所有功劳。我对他的代码进行了以下修改:
- 添加了性能改进,修复了错误,删除了不必要的代码。
- 添加了结构化异常处理,而不是返回错误代码。
- 实现了 Dime 附件而不是字节数组。这提高了性能并减少了带宽。原因是字节在作为数组发送到普通 Web 方法时会序列化为 XML。Dime 允许未序列化的二进制字节作为附件发送到 XML 消息,从而避免了在序列化和反序列化过程中 XML 应用的昂贵“填充”。
- 添加了对非常大文件的支持。
替代方法
在您将此集成到应用程序之前,请查看其他可能节省您时间和精力的方法:
- 使用 Web 服务增强功能 (WSE) 的 Dime 分块功能使用 TCP 或 UDP。WSE 版本 2 支持“分块”,其中附件被分成小部分并单独发送,但这在 HTTP 上不起作用,而许多应用程序更喜欢使用 HTTP 来解决防火墙和部署问题。如果您可以使用 TCP 或 UDP,您很可能应该选择该路线。
- 如果您只是想要一个普通 Web 服务的进度条,并且不担心超时或请求大小限制,Matt Powel 有一个文档齐全的解决方案,它会拦截传出流量并报告回您的进度条。我发现这个解决方案有点混乱,因为您需要向应用程序添加 SOAP 扩展,这使我的部署方案复杂化。但据我所知,他的解决方案无法解决 IIS 超时或最大请求长度问题。
分步概述
在本文提供的演示应用程序中,有一个 Windows Forms 应用程序,它连接到一个 Web 应用程序上的 Web 服务。文件从客户端应用程序发送,通过 Web 服务保存在 Web 服务器上。
- 用户在 Win-Forms 应用程序中选择一个文件,并可以指定上传每个块的大小。然后用户单击上传按钮。
- 在服务器上创建一个“实例”对象,该对象负责管理服务器上的上传并跟踪文件。该对象保留在服务器上的会话状态中,供客户端应用程序调用的每个
AppendChunk()
方法使用。 - 上传在工作线程上启动,该线程一个接一个地发送块,直到上传完成。服务器接收每个块并将其追加到文件中。进行了一些错误检查,以确保块在正确的位置追加。
- 工作线程每次发送一个块时都会引发一个事件,以允许用户界面更新进度条和/或状态栏消息。
- 上传完成后,从会话状态中删除“实例”对象,以释放服务器上的内存。
性能
在我的本地网络环境测试中,我上传了一个 2.5 GB 的 DVD ISO 映像,分成 16 MB 的块,耗时不到 8 分钟。(我不得不将一些数据类型从 int
改为 long
来适应巨大的文件大小!)16 MB 的块显然非常大,但在内网环境中可能很合适。在我的 CMS 应用程序中,有通过拨号连接的客户端,我将块大小设置为 32 Kb,这在整个上传过程中为用户提供了快速反馈。
在一些性能分析中,我注意到对于快速网络,块大小对每个 AppendChunk()
操作的耗时没有明显影响。因此,最好选择网络可以容纳的最大块大小,同时仍能提供合理的进度条更新频率。
安装要求
- 一个名为 UploadWeb 的虚拟目录,它应该指向 UploadWeb 文件夹。
- 对虚拟目录的匿名访问权限。
- 对虚拟目录中“Uploads”文件夹的写入权限。
可能的编译错误
如果您想编辑源代码并重新编译应用程序,您必须从 Microsoft 下载 Visual Studio 的 WSE(Web 服务增强功能)。一些用户在更新 Web 服务引用时遇到了问题,因为如果没有 WSE,Visual Studio 就不会创建 WSE 版本的代理类。如果您遇到以下任何编译错误,那么未为 Visual Studio 安装 WSE 可能是问题所在。
- 'BufferedUploadWin.BufferedUpload' 不包含 'RequestSoapContext' 的定义
- 类型或命名空间名称 'UploadWse' 在类或命名空间 'UploadWin.BufferedUploadServer' 中不存在(您是否缺少程序集引用?)
如果您在使用 WSE 时遇到问题,请访问 MSDN 的 WSE 网站。
代码摘要
- 步骤 1:用户单击上传按钮
首先,我们创建一个“
BufferedUpload
”对象。这是 WinForms 应用程序用于将文件块发送到 Web 服务的辅助类。从下面的代码可以看到,块大小设置为“块大小”Domain-Up-Down 控件中的值。添加了两个事件处理程序,一个用于更新进度条,另一个用于响应上传状态的变化,例如Completed
、Failed
等。“BeginUpload()
”方法是异步的,因此上传期间应用程序保持响应。ws = new BufferedUpload(); ws.ChunkSize = Int32.Parse(this.dudChunkSize.Text)*1000; // kilobytes ws.ProgressChanged += new ProgressChangedEventHandler(Upload_ProgressChanged); ws.StateChanged += new EventHandler(Upload_StateChanged); ws.BeginUploadChunks(this.textBox1.Text);
- 步骤 2:初始化服务器上的“Instance”对象并开始上传块
下面的代码概述了上传例程的一个简化版本。它本质上是一个
while
循环,它不断地将字节读入缓冲区,将字节作为 Dime 附件(通过MemoryStream
)附加,并将附件发送到 Web 服务。有关代码的进一步解释可以在源代码中的注释中找到。FileStream fs = new FileStream(Filename, FileMode.Open, FileAccess.Read); int bytesRead = 0; // used to store the number of bytes written each time byte[] buffer = new byte
; if (this.sentBytes == 0) { // initialise an upload Instance to manage the file I/O for this upload this.instanceId = this.Initialize(Path.GetFileName(Filename), this.overwrite); } bytesRead = fs.Read(buffer, 0, bufferSize); while (bytesRead > 0 && this.state != UploadState.Aborting) { // copy the byte buffer into a memory stream // to send in a dime attachment MemoryStream ms = new MemoryStream(buffer, 0, buffer.Length); DimeAttachment dimeAttach = new DimeAttachment("image/gif", TypeFormat.MediaType, ms); this.RequestSoapContext.Attachments.Add(dimeAttach); this.AppendChunk(this.instanceId, sentBytes, bytesRead); this.sentBytes += bytesRead; bytesRead = fs.Read(buffer, 0, bufferSize); // read the next chunk (if it } - 步骤 3:(在服务器上)将每个块追加到文件
AppendChunk()
Web 方法简单地将 Dime 附件读回byte[]
并调用Instance.AppendChunk()
方法,如下所示:public void AppendChunk(byte[] buffer, long offset, int bufferSize) { // make sure that the file exists if (!System.IO.File.Exists(this.tempFilename)) CustomSoapException("Cannot Find Temp File", this.tempFilename); // if the temp file size is not the same // as the offset then something went wrong.... if (tempFilesize != offset) CustomSoapException("Transfer Corrupted", String.Format("The file size is {0}, expected {1} bytes", tempFilesize, offset)); else { // offset matches the filesize, so the chunk // is to be inserted at the end of the file. FileStream FileStream = new FileStream(this.tempFilename, FileMode.Append); FileStream.Write(buffer, 0, bufferSize); FileStream.Close(); } }
将此代码集成到您自己的应用程序中
在您的 Win-Forms 应用程序中,添加“BufferUpload.cs”文件,并遵循演示应用程序中使用的逻辑。您不需要在 PC 上安装 WSE2 即可使其正常工作,因为 DLL 已随演示应用程序一起打包。只需在您的 WinForms 和 WebForms 项目中引用此 DLL 即可。
在您的 Web 应用程序中,添加“Instance.cs”和“upload.asmx”文件(带有 .asmx 的代码隐藏),并确保 Win 应用程序引用了您的 Web 服务的正确 URL。还要确保在您的 web.config 文件中包含 WSE 部分(从演示应用程序中的那个复制)。
Windows Forms 应用程序中的附加功能
您会注意到在演示应用程序中,状态栏的左侧会显示一个等待光标图标。这除了进度条之外,还能向用户传达一种“繁忙”的感觉。我从 FotoVision .NET 示例应用程序借用了这个技巧。另外,如果您想知道,进度条实际上并不是状态栏的一部分,它只是放置在状态栏上方并固定在角落。我将这两个功能组合成一个“FormBar
”控件,它应该是 Win-Forms 应用程序的一种通用进度工具。它提供了更新状态栏消息和进度条的方法,这些方法可以安全地从 UI 线程或工作线程调用。
我还添加了一个简单的“重试”功能。如果由于任何原因上传失败,它会弹出一个表单,并在块未能到达时从最后一个成功的位置继续。要测试这一点,请在传输过程中暂停您的 Web 服务器,您将看到重试窗口弹出。它会一直失败,直到您恢复 Web 服务器。
结论
在过去的两年里,我断断续续地花了大量精力寻找一种适合通过 HTTP 上传大文件的解决方案,我认为这是迄今为止最简单、最有效的解决方案。它非常健壮,并且支持续传功能是一个额外的优点。另外,能够可靠地上传 DVD ISO 映像,在我看来也相当不错!
注释
如果您有任何问题/建议,请在下方发布。
尽情享用!