傳說中的WCF(9):流與文件傳輸
在使用Socket/TCP來傳輸文件,弄起來不僅會有些復(fù)雜,而且較經(jīng)典的“粘包”問題有時候會讓人火冒七丈。如果你不喜歡用Socket來傳文件,不妨試試WCF,WCF的流模式傳輸還是相當強大和相當實用的。
因為開啟流模式是基于綁定的,所以,它會影響到整個終結(jié)點的操作協(xié)定。如果你不記得或者說不喜歡背書,不想去記住哪些綁定支持流模式,可以通過以下方法:
因為開啟流模式,主要是設(shè)置一個叫TransferMode的屬性,所以,你看看哪些Binding的派生類有這個屬性就可以了。
TransferMode其實是一個舉枚,看看它的幾個有效值:
1,Buffered:緩沖模式,說白了就是在內(nèi)存中緩沖,一次調(diào)用就把整個消息讀/寫完,也就是我們最常用的方式,就是普通的操作協(xié)定的調(diào)用方式;
2,StreamedRequest:只是在請求的時候使用流,說簡單一點就是在傳入方法的參數(shù)使用流,如 int MyMethod(System.IO.Stream stream);
3,StreamedResponse:就是操作協(xié)定方法返回一個流,如 Stream MyMethod(string file_name);
一般而言,如果使用流作為傳入?yún)?shù),最好不要使用多個參數(shù),如這樣:
bool TransferFile(Stream stream, string name);
上面的方法就有了兩個in參數(shù)了,最好別這樣,為什么?有空的話,自己試試就知道了。那如果要傳入更多的數(shù)據(jù),怎么辦?呵呵,還記得消息協(xié)定嗎?
好的,下面我們來弄一個上傳MP3文件的實例。實例主要的工作是從客戶端上傳一個文件到服務(wù)器。
老規(guī)矩,一般做這種應(yīng)用程序,應(yīng)該先做服務(wù)器端。
class Program
{
static void Main(string[] args)
{
// 服務(wù)器基址
Uri baseAddress = new Uri("http://localhost:1378/services");
// 聲明服務(wù)器主機
using (ServiceHost host = new ServiceHost(typeof(MyService), baseAddress))
{
// 添加綁定和終結(jié)點
BasicHttpBinding binding = new BasicHttpBinding();
// 啟用流模式
binding.TransferMode = TransferMode.StreamedRequest;
binding.MaxBufferSize = 1024;
// 接收消息的最大范圍為500M
binding.MaxReceivedMessageSize = 500 * 1024 * 1024;
host.AddServiceEndpoint(typeof(IService), binding, "/test");
// 添加服務(wù)描述
host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
try
{
// 打開服務(wù)
host.Open();
Console.WriteLine("服務(wù)已啟動。");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
}
}
[ServiceContract(Namespace = "MyNamespace")]
class IService
{
[OperationContract]
bool UpLoadFile(System.IO.Stream streamInput);
}
class MyService : IService
{
public bool UpLoadFile(System.IO.Stream streamInput)
{
bool isSuccessed = false;
try
{
using (FileStream outputStream = new FileStream("test.mp3", FileMode.OpenOrCreate, FileAccess.Write))
{
// 我們不用對兩個流對象進行讀寫,只要復(fù)制流就OK
streamInput.CopyTo(outputStream);
outputStream.Flush();
isSuccessed = true;
Console.WriteLine("在{0}接收到客戶端發(fā)送的流,已保存到test.map3。", DateTime.Now.ToLongTimeString());
}
}
catch
{
isSuccessed = false;
}
return isSuccessed;
}
}
從例子我們看到,操作方法是這樣定義的:
bool UpLoadFile(System.IO.Stream streamInput)
因為它的返回值是bool類型,不是流,而只是傳入的參數(shù)是流,因為在配置綁定時,應(yīng)用使用StreamedRequest。
BasicHttpBinding binding = new BasicHttpBinding(); // 啟用流模式 binding.TransferMode = TransferMode.StreamedRequest; binding.MaxBufferSize = 1024; // 接收消息的最大范圍為500M binding.MaxReceivedMessageSize = 500 * 1024 * 1024;
現(xiàn)在,我們做客戶端,因為要選擇文件上傳,所以使用wpf項目類型。
在窗口上拖兩個按鈕,一個用來選擇文件,另一個用于啟動文件上傳,另外兩個Label就是用來顯示一些文本。

而窗體的實現(xiàn)代碼部分如下:
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
dlg.Filter = "MP3音頻文件|*.mp3";
if (dlg.ShowDialog() == true)
{
this.label1.Content = dlg.FileName;
this.label2.Content = "準備就緒。";
}
}
private void button2_Click(object sender, RoutedEventArgs e)
{
if (!File.Exists((string)this.label1.Content))
{
return;
}
FileStream fs = new FileStream((string)this.label1.Content, FileMode.Open, FileAccess.Read);
ServiceReference1.ServiceClient cl = new ServiceReference1.ServiceClient();
this.button2.IsEnabled = false;
bool res = cl.UpLoadFile(fs);
this.button2.IsEnabled = true;
if (res == true)
this.label2.Content = "上傳完成。";
}
}
記住,千萬別忘了引用服務(wù)?。。。。。。。。。。。。。。。。。。?/strong>
現(xiàn)在可以運行了。


不知道大家注意到?jīng)]有?在服務(wù)器端代碼中,我們設(shè)置了綁定的MaxReceivedMessageSize為500M,這一般是在消息模式下,為了安全(防止惡意攻擊)而設(shè)置的限制,那么,如果使用了流模式,這個值還用不用設(shè)置。想驗證也很簡單,把這行代碼注釋掉,再運行試試。
// 接收消息的最大范圍為500M
//binding.MaxReceivedMessageSize = 500 * 1024 * 1024;
運行程序,結(jié)發(fā)現(xiàn),是不成功的,你看看我下面的截圖,只傳了40多K,還遠著呢。

因此,MaxReceivedMessageSize還是要設(shè)置的,不然,它的默認值太小了,傳不了大文件。
現(xiàn)在又希望上面的例子多一個功能,文件上傳后,依然按客戶端原文件命名,而不是test.mp3,這就意味著操作方法要傳兩個參數(shù),前面我提了一下,不要忘了消息協(xié)定,而這個我們可以通過消息協(xié)定來完成。
因此,服務(wù)器端代碼要改一改了,首先,定義一個消息協(xié)定。
[MessageContract]
public class TransferFileMessage
{
[MessageHeader]
public string File_Name; //文件名
[MessageBodyMember]
public Stream File_Stream; //文件流
}
接著操作方法也要改動。
public bool UpLoadFile(TransferFileMessage tMsg)
{
bool isSuccessed = false;
if (tMsg == null || tMsg.File_Stream == null)
{
return false;
}
try
{
using (FileStream outputStream = new FileStream(tMsg.File_Name, FileMode.OpenOrCreate, FileAccess.Write))
{
// 我們不用對兩個流對象進行讀寫,只要復(fù)制流就OK
tMsg.File_Stream.CopyTo(outputStream);
outputStream.Flush();
isSuccessed = true;
Console.WriteLine("在{0}接收到客戶端發(fā)送的流,已保存到{1}。", DateTime.Now.ToLongTimeString(), tMsg.File_Name);
}
}
catch
{
isSuccessed = false;
}
return isSuccessed;
}
在測試服務(wù)器端運行成功后,要記得更新客戶端的引用。
可是,遺憾的是,服務(wù)沒有正常啟動。為什么呢?想一想,如果光看錯誤消息,你可能不太明白。我給你20秒的時間想一想,為什么上面的代碼不能正常運行。

好了,其實,問題就出在操作協(xié)定的定義上:
[OperationContract]
bool UpLoadFile(TransferFileMessage tMsg);
我們前面說過,什么叫雙工,有來有往,是吧?對啊,上面的方法是有傳入?yún)?shù),也有返回值,有來有去啊,是雙工啊,為啥不行了呢?
哈哈,問題就在于我們使用了消息協(xié)定,在這種前提下,我們的方法就不能隨便定義了,使用消息協(xié)定的方法,如果:
a、消息協(xié)定作為傳入?yún)?shù),則只能有一個參數(shù),以下定義是錯誤的:
void Reconcile(BankingTransaction bt1, BankingTransaction bt2);
b、除非你返回值為void,如不是,那你必須返回一個消息協(xié)定,bool UpLoadFile(TransferFileMessage tMsg)我們這個定義明顯不符合要求。
那如何解決呢?我們要再定義一個用于返回的消息協(xié)定。
[MessageContract]
public class ResultMessage
{
[MessageHeader]
public string ErrorMessage;
[MessageBodyMember]
public bool IsSuccessed;
}
然后把上面的操作方法也改一下。
public ResultMessage UpLoadFile(TransferFileMessage tMsg)
{
ResultMessage rMsg = new ResultMessage();
if (tMsg == null || tMsg.File_Stream == null)
{
rMsg.ErrorMessage = "傳入的參數(shù)無效。";
rMsg.IsSuccessed = false;
return rMsg;
}
try
{
using (FileStream outputStream = new FileStream(tMsg.File_Name, FileMode.OpenOrCreate, FileAccess.Write))
{
// 我們不用對兩個流對象進行讀寫,只要復(fù)制流就OK
tMsg.File_Stream.CopyTo(outputStream);
outputStream.Flush();
rMsg.IsSuccessed = true;
Console.WriteLine("在{0}接收到客戶端發(fā)送的流,已保存到{1}。", DateTime.Now.ToLongTimeString(), tMsg.File_Name);
}
}
catch (Exception ex)
{
rMsg.IsSuccessed = false;
rMsg.ErrorMessage = ex.Message;
}
return rMsg;
}
現(xiàn)在你試試能不能正常運行?好了,客戶端記得更新引用,而且,客戶端的代碼也要修改。
private void button2_Click(object sender, RoutedEventArgs e)
{
if (!File.Exists((string)this.label1.Content))
{
return;
}
FileStream fs = new FileStream((string)this.label1.Content, FileMode.Open, FileAccess.Read);
ServiceReference1.ServiceClient cl = new ServiceReference1.ServiceClient();
this.button2.IsEnabled = false;
bool isSuccessed = false;
var response = cl.UpLoadFile(System.IO.Path.GetFileName((string)this.label1.Content), fs, out isSuccessed);
this.button2.IsEnabled = true;
if (isSuccessed == true)
this.label2.Content = "上傳完成。";
else
this.label2.Content = "錯誤信息:" + response;
}
現(xiàn)在再來測測吧。

再看看服務(wù)器端。


哈哈,現(xiàn)在就完美解決了。

浙公網(wǎng)安備 33010602011771號