当有大量的数据要传输(例如文件的上传和下载)时,WCF的流模式是比较好的选择,因为流模式不是全部传输完后才响应,而是一边读取一边传输消息,改善了系统的吞吐量和响应效率。
但是,WCF对于Stream操作有一些限制:
1. 绑定的限制。由于低层协议特性限制,WCF的流模式只支持如下四种:
- BasicHttpBinding
- NetTcpBinding
- NetNamedPipeBinding
- WebHttpBinding
2. OperationContract接口的限制。若要对数据进行流处理,服务的 OperationContract 必须满足两个要求:
- 最多只能有一个参数。
- 参数和返回值的类型中至少有一个必须是 Stream, Message 或 IXmlSerializable。
3. 会话模式限制。
当InstanceContextMode设置为Single或PerSession的时,多个服务请求可能会使用同一数据通道。此时就无法实现并发下载,只能依次顺序执行,而这往往不是我们所期望的结果。因此,对于使用流模式的服务,需要将InstanceContextMode设置为PerCall。
如下是几个有效的示例:
[ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples")] public interface IstreamingSample { [OperationContract] Stream GetStream(string data); [OperationContract] bool UploadStream(Stream stream); [OperationContract] Stream EchoStream(Stream stream); [OperationContract] Stream GetReversedStream(); }
如果有许多信息需要作为参数传递,可以使用MessageContract构造一个复杂点的消息。
[MessageContract] public class UploadStreamMessage { [MessageHeader] public string id; [MessageBodyMember] public Stream data; }
这个参数的限制并不难理解:
- 一次请求-响应之间只发送一个数据包,本质上来说只能携带一个参数。
- 在缓冲模式下,系统可以把多个参数全部加载到内存中后,然后通过序列化的方式将其合并为一个数据包再发送,这样看起来可以发送多个参数了。
- 流模式并不能使用这种方案,因此顶多只能发一个参数。要发多个参数,必须自己定义数据包格式(需要满足 MessageContract ),将多个参数整合到一个参数中发送。
下面我就以一个文件上传为例,简单的演示一下如何实现流模式数据传输。
一. 服务器端修改配置
- 设置TransferMode。它支持四种模式(Buffered、Streamed、StreamedRequest、StreamedResponse),请根据具体情况设置成三种Stream模式之一。
- 修改MaxReceivedMessageSize。该值默认大小为64k,因此,当传输数据大于64k时,则抛出CommunicationException异常。(可以直接设置为int.max)
- 修改receiveTimeout 和sendTimeout。大数据传送时间较长,需要修改这两个值,以免传输超时。
<basicHttpBinding> <binding name="BasicBinding" receiveTimeout="00:30:00" sendTimeout="00:30:00" maxReceivedMessageSize="104857600" transferMode="Streamed" /> </basicHttpBinding>
二. 定义契约,并实现服务
这个接口很简单,就是实现上传一个文件:
[ServiceContract] public interface IService1 { [OperationContract] Task UploadFile(Stream stream); }
由于接口中除了Stream没有其它的有限参数,我这里的实现也很简单,只是直接把它保存为1.jpg。
public class Service1 : IService1 { public async Task UploadFile(Stream stream) { using (stream) using (var file = File.Create(@"R:\server\1.jpg")) { await stream.CopyToAsync(file); } } }
三. 访问服务
客户端的实现一如既往的简单,为了示例简单,这里客户端是以同步的方式访问的。
static void Main(string[] args) { var client = new WcfClient.Service.Service1Client(); using (var file = File.OpenRead(@"R:\client\1.jpg")) { client.UploadFile(file); } Console.WriteLine("finished"); }
四. 扩展功能
前面的例子作为文件上传还是不够的,主要存在如下两个缺点:
- 不能指定文件名
- 不能获取上传进度
首先来解决文件名问题,不能指定文件名的主要原因就是消息体中对参数个数有限制,因此,必须把文件名和stream放在一起作为参数传入。这个只需要用MessageContract定义一个消息即可:
[MessageContract] public class UploadStreamMessage { [MessageHeader] public string Name { get; set; } [MessageBodyMember] public Stream Stream { get; set; } }
这样,把UploadStreamMessage作为参数,就可以携带文件名信息了。
[ServiceContract] public interface IService1 { [OperationContract] Task UploadFile(UploadStreamMessage msg); } public class Service1 : IService1 { public async Task UploadFile(UploadStreamMessage msg) { using (msg.Stream) using (var file = File.Create(@"R:\server\" + msg.Name)) { await msg.Stream.CopyToAsync(file); } } }
重新启动服务后,更新客户端,此时就会发现生成的代码中都把参数给分离出来了,非常贴心。
static void Main(string[] args) { var client = new WcfClient.Service.Service1Client(); using (var file = File.OpenRead(@"R:\client\1.jpg")) { client.UploadFile("test.jpg", file); } Console.WriteLine("finished"); }
至于当前上传了多少数据,直接取一下FileStream的Position就可以了,就不用远程服务器提供接口了。
PS:关于大数据传输,流模式并非唯一选择,我这里也只是对流模式进行了蜻蜓点水般的介绍,更多信息可以参看MSDN的相关文章:1. ,2. 。CodeProject上的文章介绍得非常详细,强烈推荐一下。