• 传说中的WCF(9):流与文件传输


    传说中的WCF(1):这东西难学吗?
    传说中的WCF(2):服务协定的那些事儿
    传说中的WCF(3):多个协定
    传说中的WCF(4):发送和接收SOAP头
    传说中的WCF(5):数据协定(a)
    传说中的WCF(6):数据协定(b)
    传说中的WCF(7):“单向”&“双向”
    传说中的WCF(8):玩转消息协定
    传说中的WCF(9):流与文件传输
    传说中的WCF(10):消息拦截与篡改
    传说中的WCF(11):会话(Session)
    传说中的WCF(12):服务器回调有啥用
    传说中的WCF(13):群聊天程序
    传说中的WCF(14):WCF也可以做聊天程序 

    在使用Socket/TCP来传输文件,弄起来不仅会有些复杂,而且较经典的“粘包”问题有时候会让人火冒七丈。如果你不喜欢用Socket来传文件,不妨试试WCF,WCF的流模式传输还是相当强大和相当实用的。

    因为开启流模式是基于绑定的,所以,它会影响到整个终结点的操作协定。如果你不记得或者说不喜欢背书,不想去记住哪些绑定支持流模式,可以通过以下方法:

    因为开启流模式,主要是设置一个叫TransferMode的属性,所以,你看看哪些Binding的派生类有这个属性就可以了。

    TransferMode其实是一个举枚,看看它的几个有效值:

    • Buffered:缓冲模式,说白了就是在内存中缓冲,一次调用就把整个消息读/写完,也就是我们最常用的方式,就是普通的操作协定的调用方式;
    • StreamedRequest:只是在请求的时候使用流,说简单一点就是在传入方法的参数使用流,如 int MyMethod(System.IO.Stream stream);
    • StreamedResponse:就是操作协定方法返回一个流,如 Stream MyMethod(string file_name);

    一般而言,如果使用流作为传入参数,最好不要使用多个参数,如这样:

    bool TransferFile(Stream stream, string name);

    上面的方法就有了两个in参数了,最好别这样,为什么?有空的话,自己试试就知道了。那如果要传入更多的数据,怎么办?呵呵,还记得消息协定吗?

    好的,下面我们来弄一个上传MP3文件的实例。实例主要的工作是从客户端上传一个文件到服务器。

    老规矩,一般做这种应用程序,应该先做服务器端。

    在使用Socket/TCP来传输文件,弄起来不仅会有些复杂,而且较经典的“粘包”问题有时候会让人火冒七丈。如果你不喜欢用Socket来传文件,不妨试试WCF,WCF的流模式传输还是相当强大和相当实用的。

    因为开启流模式是基于绑定的,所以,它会影响到整个终结点的操作协定。如果你不记得或者说不喜欢背书,不想去记住哪些绑定支持流模式,可以通过以下方法:

    因为开启流模式,主要是设置一个叫TransferMode的属性,所以,你看看哪些Binding的派生类有这个属性就可以了。

    TransferMode其实是一个举枚,看看它的几个有效值:

    • Buffered:缓冲模式,说白了就是在内存中缓冲,一次调用就把整个消息读/写完,也就是我们最常用的方式,就是普通的操作协定的调用方式;
    • StreamedRequest:只是在请求的时候使用流,说简单一点就是在传入方法的参数使用流,如 int MyMethod(System.IO.Stream stream);
    • StreamedResponse:就是操作协定方法返回一个流,如 Stream MyMethod(string file_name);

    一般而言,如果使用流作为传入参数,最好不要使用多个参数,如这样:

    bool TransferFile(Stream stream, string name);

    上面的方法就有了两个in参数了,最好别这样,为什么?有空的话,自己试试就知道了。那如果要传入更多的数据,怎么办?呵呵,还记得消息协定吗?

    好的,下面我们来弄一个上传MP3文件的实例。实例主要的工作是从客户端上传一个文件到服务器。

    老规矩,一般做这种应用程序,应该先做服务器端。

    1. using System;  
    2. using System.Collections.Generic;  
    3. using System.Linq;  
    4. using System.Text;  
    5. using System.Threading.Tasks;  
    6.   
    7. using System.Runtime;  
    8. using System.Runtime.Serialization;  
    9. using System.ServiceModel;  
    10. using System.ServiceModel.Description;  
    11.   
    12. using System.IO;  
    13.   
    14. namespace WCFServerTemplate1  
    15. {  
    16.     class Program  
    17.     {  
    18.         static void Main(string[] args)  
    19.         {  
    20.             // 服务器基址  
    21.             Uri baseAddress = new Uri("http://localhost:1378/services");  
    22.             // 声明服务器主机  
    23.             using (ServiceHost host = new ServiceHost(typeof(MyService), baseAddress))  
    24.             {  
    25.                 // 添加绑定和终结点  
    26.                 BasicHttpBinding binding = new BasicHttpBinding();  
    27.                 // 启用流模式  
    28.                 binding.TransferMode = TransferMode.StreamedRequest;  
    29.                 binding.MaxBufferSize = 1024;  
    30.                 // 接收消息的最大范围为500M  
    31.                 binding.MaxReceivedMessageSize = 500 * 1024 * 1024;  
    32.                 host.AddServiceEndpoint(typeof(IService), binding, "/test");  
    33.                 // 添加服务描述  
    34.                 host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });  
    35.                 try  
    36.                 {  
    37.                     // 打开服务  
    38.                     host.Open();  
    39.                     Console.WriteLine("服务已启动。");  
    40.                 }  
    41.                 catch (Exception ex)  
    42.                 {  
    43.                     Console.WriteLine(ex.Message);  
    44.                 }  
    45.                 Console.ReadKey();  
    46.             }  
    47.         }  
    48.     }  
    49.   
    50.     [ServiceContract(Namespace = "MyNamespace")]  
    51.     public interface IService  
    52.     {  
    53.         [OperationContract]  
    54.         bool UpLoadFile(System.IO.Stream streamInput);  
    55.     }  
    56.   
    57.     public class MyService : IService  
    58.     {  
    59.   
    60.         public bool UpLoadFile(System.IO.Stream streamInput)  
    61.         {  
    62.             bool isSuccessed = false;  
    63.             try  
    64.             {  
    65.                 using (FileStream outputStream = new FileStream("rec.mp3", FileMode.OpenOrCreate, FileAccess.Write))  
    66.                 {  
    67.                     // 我们不用对两个流对象进行读写,只要复制流就OK  
    68.                     streamInput.CopyTo(outputStream);  
    69.                     outputStream.Flush();  
    70.                     isSuccessed = true;  
    71.                     Console.WriteLine("在{0}接收到客户端发送的流,已保存到rec.map3。",DateTime.Now.ToLongTimeString());  
    72.                 }  
    73.             }  
    74.             catch  
    75.             {  
    76.                 isSuccessed = false;  
    77.             }  
    78.             return isSuccessed;  
    79.         }  
    80.     }  
    81.   
    82. }  


    从例子我们看到,操作方法是这样定义的:

    1. bool UpLoadFile(System.IO.Stream streamInput)  

    因为它的返回值是Bool类型,不是流,而只是传入的参数是流,因为在配置绑定时,应用使用StreamedRequest。

    1. BasicHttpBinding binding = new BasicHttpBinding();  
    2. // 启用流模式  
    3. binding.TransferMode = TransferMode.StreamedRequest;  
    4. binding.MaxBufferSize = 1024;  
    5. // 接收消息的最大范围为500M  
    6. binding.MaxReceivedMessageSize = 500 * 1024 * 1024;  


    现在,我们做客户端,因为要选择文件上传,所以使用Windows Forms项目类型。

    在窗口上拖两个按钮,一个用来选择文件,另一个用于启动文件上传,另外两个Label就是用来显示一些文本。

    而窗体的实现代码部分如下:

    1. using System;  
    2. using System.Collections.Generic;  
    3. using System.ComponentModel;  
    4. using System.Data;  
    5. using System.Drawing;  
    6. using System.Linq;  
    7. using System.Text;  
    8. using System.Threading.Tasks;  
    9. using System.Windows.Forms;  
    10. using System.IO;  
    11.   
    12. namespace wformClient  
    13. {  
    14.     public partial class Form1 : Form  
    15.     {  
    16.         public Form1()  
    17.         {  
    18.             InitializeComponent();  
    19.         }  
    20.   
    21.         private void btnSelectFile_Click(object sender, EventArgs e)  
    22.         {  
    23.             OpenFileDialog dlg = new OpenFileDialog();  
    24.             dlg.Filter = "MP3音频文件|*.mp3";  
    25.             if (DialogResult.OK.Equals(dlg.ShowDialog()))  
    26.             {  
    27.                 this.lbSelectedFilename.Text = dlg.FileName;  
    28.                 this.lbMessage.Text = "准备就绪。";  
    29.             }  
    30.         }  
    31.   
    32.         private async void btnTransfer_Click(object sender, EventArgs e)  
    33.         {  
    34.             if (!File.Exists(this.lbSelectedFilename.Text))  
    35.             {  
    36.                 return;  
    37.             }  
    38.             FileStream fs = new FileStream(this.lbSelectedFilename.Text, FileMode.Open, FileAccess.Read);  
    39.             WS.ServiceClient cl = new WS.ServiceClient();  
    40.             this.btnTransfer.Enabled = false;  
    41.             bool res = await cl.UpLoadFileAsync(fs);  
    42.             this.btnTransfer.Enabled = true;  
    43.             if (res == true)  
    44.                 this.lbMessage.Text = "上传完成。";  
    45.         }  
    46.     }  
    47. }  


    记住,千万别忘了引用服务!!!!!!!!!!!!!!!!!!!

    现在可以运行了。

    不知道大家注意到没有?在服务器端代码中,我们设置了绑定的MaxReceivedMessageSize为500M,这一般是在消息模式下,为了安全(防止恶意攻击)而设置的限制,那么,如果使用了流模式,这个值还用不用设置。想验证也很简单,把这行代码注释掉,再运行试试。

    运行程序,结发现,是不成功的,你看看我下面的截图,只传了40多K,还远着呢。

    因此,MaxReceivedMessageSize还是要设置的,不然,它的默认值太小了,传不了大文件。

    现在又希望上面的例子多一个功能,文件上传后,依然按客户端原文件命名,而不是rec.mp3,这就意味着操作方法要传两个参数,前面我提了一下,不要忘了消息协定,而这个我们可以通过消息协定来完成。

    因此,服务器端代码要改一改了,首先,定义一个消息协定。

    1. [MessageContract]  
    2. public class TransferFileMessage  
    3. {  
    4.     [MessageHeader]  
    5.     public string File_Name; //文件名  
    6.     [MessageBodyMember]  
    7.     public Stream File_Stream; //文件流  
    8. }  


    接着操作方法也要改动。

    1. [ServiceContract(Namespace = "MyNamespace")]  
    2. public interface IService  
    3. {  
    4.     [OperationContract]  
    5.     bool UpLoadFile(TransferFileMessage tMsg);  
    6. }  
    7.   
    8. public class MyService : IService  
    9. {  
    10.   
    11.     public bool UpLoadFile(TransferFileMessage tMsg)  
    12.     {  
    13.         bool isSuccessed = false;  
    14.         if (tMsg == null || tMsg.File_Stream == null)  
    15.         {  
    16.             return false;  
    17.         }  
    18.         try  
    19.         {  
    20.             using (FileStream outputStream = new FileStream(tMsg.File_Name, FileMode.OpenOrCreate, FileAccess.Write))  
    21.             {  
    22.                 // 我们不用对两个流对象进行读写,只要复制流就OK  
    23.                 tMsg.File_Stream.CopyTo(outputStream);  
    24.                 outputStream.Flush();  
    25.                 isSuccessed = true;  
    26.                 Console.WriteLine("在{0}接收到客户端发送的流,已保存到{1}。",DateTime.Now.ToLongTimeString(), tMsg.File_Name);  
    27.             }  
    28.         }  
    29.         catch  
    30.         {  
    31.             isSuccessed = false;  
    32.         }  
    33.         return isSuccessed;  
    34.     }  
    35. }  


    在测试服务器端运行成功后,要记得更新客户端的引用。

    可是,遗憾的是,服务没有正常启动。为什么呢?想一想,如果光看错误消息,你可能不太明白。我给你20秒的时间想一想,为什么上面的代码不能正常运行。

    ………………………………………………………………………………………………………………………………………………

    好了,其实,问题就出在操作协定的定义上:

    1. [OperationContract]  
    2. bool UpLoadFile(TransferFileMessage tMsg);  


    我们前面说过,什么叫双工,有来有往,是吧?对啊,上面的方法是有传入参数,也有返回值,有来有去啊,是双工啊,为啥不行了呢?

    哈哈,问题就在于我们使用了消息协定,在这种前提下,我们的方法就不能随便定义了,使用消息协定的方法,如果:

    a、消息协定作为传入参数,则只能有一个参数,以下定义是错误的:

    void Reconcile(BankingTransaction bt1, BankingTransaction bt2);

    b、除非你返回值为void,如不是,那你必须返回一个消息协定,bool UpLoadFile(TransferFileMessage tMsg)我们这个定义明显不符合要求。

    那如何解决呢?我们要再定义一个用于返回的消息协定。

    1. [MessageContract]  
    2. public class ResultMessage  
    3. {  
    4.     [MessageHeader]  
    5.     public string ErrorMessage;  
    6.     [MessageBodyMember]  
    7.     public bool IsSuccessed;  
    8. }  


    然后把上面的操作方法也改一下。

    1. public ResultMessage UpLoadFile(TransferFileMessage tMsg)  
    2. {  
    3.     ResultMessage rMsg = new ResultMessage();  
    4.     if (tMsg == null || tMsg.File_Stream == null)  
    5.     {  
    6.         rMsg.ErrorMessage = "传入的参数无效。";  
    7.         rMsg.IsSuccessed = false;  
    8.         return rMsg;  
    9.     }  
    10.     try  
    11.     {  
    12.         using (FileStream outputStream = new FileStream(tMsg.File_Name, FileMode.OpenOrCreate, FileAccess.Write))  
    13.         {  
    14.             // 我们不用对两个流对象进行读写,只要复制流就OK  
    15.             tMsg.File_Stream.CopyTo(outputStream);  
    16.             outputStream.Flush();  
    17.             rMsg.IsSuccessed = true;  
    18.             Console.WriteLine("在{0}接收到客户端发送的流,已保存到{1}。", DateTime.Now.ToLongTimeString(), tMsg.File_Name);  
    19.         }  
    20.     }  
    21.     catch(Exception ex)  
    22.     {  
    23.         rMsg.IsSuccessed = false;  
    24.         rMsg.ErrorMessage = ex.Message;  
    25.     }  
    26.     return rMsg;  
    27. }  


    现在你试试能不能正常运行?好了,客户端记得更新引用,而且,客户端的代码也要修改。

    1. private async void btnTransfer_Click(object sender, EventArgs e)  
    2. {  
    3.     if (!File.Exists(this.lbSelectedFilename.Text))  
    4.     {  
    5.         return;  
    6.     }  
    7.     FileStream fs = new FileStream(this.lbSelectedFilename.Text, FileMode.Open, FileAccess.Read);  
    8.     WS.ServiceClient cl = new WS.ServiceClient();  
    9.     this.btnTransfer.Enabled = false;  
    10.     var response = await cl.UpLoadFileAsync(Path.GetFileName(this.lbSelectedFilename.Text), fs);  
    11.     this.btnTransfer.Enabled = true;  
    12.     if (response.IsSuccessed == true)  
    13.         this.lbMessage.Text = "上传完成。";  
    14.     else  
    15.         this.lbMessage.Text="错误信息:" + response.ErrorMessage;  
    16. }  


    现在再来测测吧。

    再看看服务器端。

    哈哈,现在就完美解决了。


    从例子我们看到,操作方法是这样定义的:

    bool UpLoadFile(System.IO.Stream streamInput)

    因为它的返回值是Bool类型,不是流,而只是传入的参数是流,因为在配置绑定时,应用使用StreamedRequest。

                    BasicHttpBinding binding = new BasicHttpBinding();
                    // 启用流模式
                    binding.TransferMode = TransferMode.StreamedRequest;
                    binding.MaxBufferSize = 1024;
                    // 接收消息的最大范围为500M
                    binding.MaxReceivedMessageSize = 500 * 1024 * 1024;


    现在,我们做客户端,因为要选择文件上传,所以使用Windows Forms项目类型。

    在窗口上拖两个按钮,一个用来选择文件,另一个用于启动文件上传,另外两个Label就是用来显示一些文本。

    而窗体的实现代码部分如下:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    using System.IO;

    namespace wformClient
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }

            private void btnSelectFile_Click(object sender, EventArgs e)
            {
                OpenFileDialog dlg = new OpenFileDialog();
                dlg.Filter = "MP3音频文件|*.mp3";
                if (DialogResult.OK.Equals(dlg.ShowDialog()))
                {
                    this.lbSelectedFilename.Text = dlg.FileName;
                    this.lbMessage.Text = "准备就绪。";
                }
            }

            private async void btnTransfer_Click(object sender, EventArgs e)
            {
                if (!File.Exists(this.lbSelectedFilename.Text))
                {
                    return;
                }
                FileStream fs = new FileStream(this.lbSelectedFilename.Text, FileMode.Open, FileAccess.Read);
                WS.ServiceClient cl = new WS.ServiceClient();
                this.btnTransfer.Enabled = false;
                bool res = await cl.UpLoadFileAsync(fs);
                this.btnTransfer.Enabled = true;
                if (res == true)
                    this.lbMessage.Text = "上传完成。";
            }
        }
    }
     


    记住,千万别忘了引用服务!!!!!!!!!!!!!!!!!!!

    现在可以运行了。

    不知道大家注意到没有?在服务器端代码中,我们设置了绑定的MaxReceivedMessageSize为500M,这一般是在消息模式下,为了安全(防止恶意攻击)而设置的限制,那么,如果使用了流模式,这个值还用不用设置。想验证也很简单,把这行代码注释掉,再运行试试。

    运行程序,结发现,是不成功的,你看看我下面的截图,只传了40多K,还远着呢。

    因此,MaxReceivedMessageSize还是要设置的,不然,它的默认值太小了,传不了大文件。

    现在又希望上面的例子多一个功能,文件上传后,依然按客户端原文件命名,而不是rec.mp3,这就意味着操作方法要传两个参数,前面我提了一下,不要忘了消息协定,而这个我们可以通过消息协定来完成。

    因此,服务器端代码要改一改了,首先,定义一个消息协定。

        [MessageContract]
        public class TransferFileMessage
        {
            [MessageHeader]
            public string File_Name; //文件名
            [MessageBodyMember]
            public Stream File_Stream; //文件流
        }


    接着操作方法也要改动。

        [ServiceContract(Namespace = "MyNamespace")]
        public interface IService
        {
            [OperationContract]
            bool UpLoadFile(TransferFileMessage tMsg);
        }

        public class MyService : IService
        {

            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))
                    {
                        // 我们不用对两个流对象进行读写,只要复制流就OK
                        tMsg.File_Stream.CopyTo(outputStream);
                        outputStream.Flush();
                        isSuccessed = true;
                        Console.WriteLine("在{0}接收到客户端发送的流,已保存到{1}。",DateTime.Now.ToLongTimeString(), tMsg.File_Name);
                    }
                }
                catch
                {
                    isSuccessed = false;
                }
                return isSuccessed;
            }
        }


    在测试服务器端运行成功后,要记得更新客户端的引用。

    可是,遗憾的是,服务没有正常启动。为什么呢?想一想,如果光看错误消息,你可能不太明白。我给你20秒的时间想一想,为什么上面的代码不能正常运行。

    ………………………………………………………………………………………………………………………………………………

    好了,其实,问题就出在操作协定的定义上:

            [OperationContract]
            bool UpLoadFile(TransferFileMessage tMsg);


    我们前面说过,什么叫双工,有来有往,是吧?对啊,上面的方法是有传入参数,也有返回值,有来有去啊,是双工啊,为啥不行了呢?

    哈哈,问题就在于我们使用了消息协定,在这种前提下,我们的方法就不能随便定义了,使用消息协定的方法,如果:

    a、消息协定作为传入参数,则只能有一个参数,以下定义是错误的:

    void Reconcile(BankingTransaction bt1, BankingTransaction bt2);

    b、除非你返回值为void,如不是,那你必须返回一个消息协定,bool UpLoadFile(TransferFileMessage tMsg)我们这个定义明显不符合要求。

    那如何解决呢?我们要再定义一个用于返回的消息协定。

        [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 = "传入的参数无效。";
                    rMsg.IsSuccessed = false;
                    return rMsg;
                }
                try
                {
                    using (FileStream outputStream = new FileStream(tMsg.File_Name, FileMode.OpenOrCreate, FileAccess.Write))
                    {
                        // 我们不用对两个流对象进行读写,只要复制流就OK
                        tMsg.File_Stream.CopyTo(outputStream);
                        outputStream.Flush();
                        rMsg.IsSuccessed = true;
                        Console.WriteLine("在{0}接收到客户端发送的流,已保存到{1}。", DateTime.Now.ToLongTimeString(), tMsg.File_Name);
                    }
                }
                catch(Exception ex)
                {
                    rMsg.IsSuccessed = false;
                    rMsg.ErrorMessage = ex.Message;
                }
                return rMsg;
            }


    现在你试试能不能正常运行?好了,客户端记得更新引用,而且,客户端的代码也要修改。

            private async void btnTransfer_Click(object sender, EventArgs e)
            {
                if (!File.Exists(this.lbSelectedFilename.Text))
                {
                    return;
                }
                FileStream fs = new FileStream(this.lbSelectedFilename.Text, FileMode.Open, FileAccess.Read);
                WS.ServiceClient cl = new WS.ServiceClient();
                this.btnTransfer.Enabled = false;
                var response = await cl.UpLoadFileAsync(Path.GetFileName(this.lbSelectedFilename.Text), fs);
                this.btnTransfer.Enabled = true;
                if (response.IsSuccessed == true)
                    this.lbMessage.Text = "上传完成。";
                else
                    this.lbMessage.Text="错误信息:" + response.ErrorMessage;
            }


    现在再来测测吧。

    再看看服务器端。

    哈哈,现在就完美解决了。

    转IT黄老邪

  • 相关阅读:
    HDU 5818 Joint Stacks
    HDU 5816 Hearthstone
    HDU 5812 Distance
    HDU 5807 Keep In Touch
    HDU 5798 Stabilization
    HDU 5543 Pick The Sticks
    Light OJ 1393 Crazy Calendar (尼姆博弈)
    NEFU 2016省赛演练一 I题 (模拟题)
    NEFU 2016省赛演练一 F题 (高精度加法)
    NEFU 2016省赛演练一 B题(递推)
  • 原文地址:https://www.cnblogs.com/jcomet/p/3058544.html
Copyright © 2020-2023  润新知