當有大量的數據要傳輸(例如文件的上傳和下載)時,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上的文章WCF Streaming: Upload/Download Files Over HTTP介紹得非常詳細,強烈推薦一下。