使用Stream上传文件
文件上传功能是web程序/服务上常用和必须的功能,WCF也不例外。在4.0版本之前,WCF仅仅提供了buffered模式上传文件。从4.0版本之后,WCF开始提供了Streaming模式。在buffered模式中,实体文件需要在WCF服务可以访问之前上传到服务。在Streaming模式中,服务在文件上传完成之前就可以访问了。当你需要通过服务对文件进行处理或者由于文件太大无法将文件进行Buffered的时候,Stream模式就非常有用了。
在本节,我们来看看如何实现和配置一个服务,上其支持Streaming模式进行文件上传。
如何做
- 启动Visual Studio2012,创建一个WCF服务库,命名为WcfFileUploadService
- 添加一个新的类,名称为UploadDetails
- 打开类文件,用[MessageContract]修饰这个类
- 将类声明为public
- 给类添加下面的属性
Name |
Data type |
FileName |
String |
Data |
Stream |
- 用[MessageHeader]修饰FileName属性
- 用[MessageBodyMember]修饰data属性
- 修改之后,UploadDetails类应该看起来是这样的
[MessageContract] public class UploadDetails { [MessageHeader] public string FileName { get; set; } [MessageBodyMember] public Stream Data { get; set; } } |
- 将IService重命名为IUploadService,将Service类重命名为UploadService
- 打开IUploadService类,移除已经存在的代码
- 添加一个方法接受一个UploadDetails类型的参数,返回类型是void,方法名为Upload,它的方法前面如下:
void Upload(UploadDetails details); |
- 用[OperationContract]修改这个方法,这个接口现在是这样
[ServiceContract] public interface IUploadService { [OperationContract] void Upload(UploadDetails details); } |
- 下一步,打开UploadService类,实现IUploadService接口。移除这个类现有的代码
- 实现IUploadService接口的Upload方法
- 添加下面的代码到Upload方法:
using (FileStream fs = new FileStream(@"C:\Downloads\"+details. FileName, FileMode.Create)) { int bufferSize = 1 * 1024 * 1024; byte[] buffer = new byte[bufferSize]; int bytes; while ((bytes = details.Data.Read(buffer, 0, bufferSize)) > 0) { fs.Write(buffer, 0, bytes); fs.Flush(); }
} |
- 在前面的代码当中的C:\Downloads可以替换为你自己允许保存文件的目录(当要有确保文件目录存在,且有读写权限)
- 现在打开App.config,在<services>节前面加入下面的binding
<bindings> <basicHttpBinding> <binding name="UploadServiceBinding" messageEncoding="Text" transferMode="Streamed" maxBufferSize="65536" maxReceivedMessageSize="5242880"> </binding> </basicHttpBinding> </bindings> |
- 将<service>节点修改为下面的样子
<service name="WcfFileUploadService.UploadService"> <endpoint address="" binding="basicHttpBinding" bindingConfiguration="UploadServiceBinding" contract="WcfFileUploadService.IUploadService"> <identity> <dns value="localhost" /> </identity> </endpoint> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <host> <baseAddresses> <add baseAddress="http://localhost:8733/Design_Time_ Addresses/WcfFileUploadService/Service1/" /> </baseAddresses> </host> </service> |
- 添加一个Windows Form Appliction命名为UploadServiceTestApp
- 将Form1.cpp重命名为UploadTestForm.cs
- 切换到设计界面,将界面按一下截图进行设置
- 将控件按下表进行命名
Control |
Name |
Description |
Textbox |
txtFile |
显示选择文件的路径 |
Button |
btnBrowse |
调用文件上传服务 |
- 添加对UploadService服务的引用,将其命名为UploadServiceReference
- 双击btnBrowse按钮,添加事件处理器
- 在事件处理过程中添加以下代码
OpenFileDialog diagOpen = new OpenFileDialog(); if (diagOpen.ShowDialog() == System.Windows.Forms.DialogResult.OK) { try { txtFile.Text = diagOpen.FileName; UploadServiceReference.UploadServiceClient client = new UploadServiceReference.UploadServiceClient(); client.Upload(Path.GetFileName(txtFile.Text), File. Open(txtFile.Text, FileMode.Open)); MessageBox.Show("Upload successful"); } catch (Exception) { MessageBox.Show("Upload failed"); } } |
- 在主菜单中点击 调试|运行新实例,你会看到以下截图
- 点击Browse and Upload按钮,选择一个文件上传。
- 如果上传成功,你会看到下面的消息框
- 如果不成功,会看到错误提示
实现原理
为了使用了Streaming模式上传文件,服务应该提供一个方法访问Stream类型的参数,有一点需要记住如果方法有一个Stream参数,就不允许有其他的参数或其他的返回值,否则服务就不能运行。这就是我们为什么要要将UploadDetails配置成为MessageContract的原因。
[MessageContract] public class UploadDetails { [MessageHeader] public string FileName { get; set; } [MessageBodyMember] public Stream Data { get; set; } } |
在前面的代码中,我们给FileName添加[MessageHeader]修饰,而不是[MessageBodyMember],这是因为Data是一个Stream类型成员。如果MessageContract包含了一个Stream类型的MessageBodyMember,其他属性只能做作为MessageHeader,换句话说,如果有一个Stream类型的属性,那么就只能有一个MessageBodyMember。其他的属性都必须是MessageHeader (那么Stream类型的成员只能有一个)。
在UploadService类的Upload方法中,我们首先打开了一个FileStream往文件夹中写文件。然后,然后我将UploadDetails实例中的Stream类型成员的数据写入到文件中。
using (FileStream fs = new FileStream(@"C:\Downloads"+details. FileName, FileMode.Create)) { int bufferSize = 1 * 1024 * 1024; byte[] buffer = new byte[bufferSize]; int bytes; while ((bytes = details.Data.Read(buffer, 0, bufferSize)) > 0) { fs.Write(buffer, 0, bytes); fs.Flush(); }
} |
为了告诉.NET Runtime我们需要使用Stream模式,我们需要添加一个新的binding命名为UploadServiceBinding。在binding中,我设置messageEncoding为Text,transferMode为Streamed,然后我们设置最大的buffer长度,以及消息大小的上限,消息的大小决定了可以上传的最大尺寸。
<bindings> <basicHttpBinding> <binding name="UploadServiceBinding" messageEncoding="Text" transferMode="Streamed" maxBufferSize="65536" maxReceivedMessageSize="5242880">
</binding> </basicHttpBinding> </bindings> |
因为我们想使用HTTP自己来传输,我们使用了<basicHttpBinding>。在<service>节点中,我们设置了bindingConfiguration为UploadServiceBinding,看下面高亮的代码
<service name="WcfFileUploadService.UploadService"> <endpoint address="" binding="basicHttpBinding" bindingConfiguration="UploadServiceBinding" contract="WcfFileUploadService.IUploadService"> <identity> <dns value="localhost" /> </identity> </endpoint> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <host> <baseAddresses> <add baseAddress="http://localhost:8733/Design_Time_Addresses/ WcfFileUploadService/Service1/" /> </baseAddresses> </host> </service> |
在UploadTestForm中,我们通过选择的文件并用文件的内容传教的FileStream实例调用服务的Upload方法。
txtFile.Text = diagOpen.FileName; UploadServiceReference.UploadServiceClient client = new UploadServiceReference.UploadServiceClient(); client.Upload(Path.GetFileName(txtFile.Text), File.Open(txtFile.Text, FileMode.Open)); |
在接口和实现中,我们使用MessageContract约束的类型作为Upload方法的参数。但是我们在客户端调用的时候,分别传递了文件名和Stream的实例,(也就是说客户端调用时方法传的参数和WCF接口声明不一致),将参数封装为MessageContract,是由.NET Runtime完成的,将其进行类型转换是在服务端完成的。
你不能在binding中同时将传输模式设置为Streamed,而将编码设置为MOTM,如果同时使用这两项设置会在上传文件的时候引发问题,问题以及引发问题的原因超出了本文论述的范围。 |