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


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

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

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

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

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

    bool TransferFile(Stream stream, string name);

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

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

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

    复制代码
        class Program
        {
            static void Main(string[] args)
            {
                // 服务器基址  
                Uri baseAddress = new Uri("http://localhost:1378/services");
                // 声明服务器主机  
                using (ServiceHost host = new ServiceHost(typeof(MyService), baseAddress))
                {
                    // 添加绑定和终结点  
                    BasicHttpBinding binding = new BasicHttpBinding();
                    // 启用流模式  
                    binding.TransferMode = TransferMode.StreamedRequest;
                    binding.MaxBufferSize = 1024;
                    // 接收消息的最大范围为500M  
                    binding.MaxReceivedMessageSize = 500 * 1024 * 1024;
                    host.AddServiceEndpoint(typeof(IService), binding, "/test");
                    // 添加服务描述  
                    host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
                    try
                    {
                        // 打开服务  
                        host.Open();
                        Console.WriteLine("服务已启动。");
                    }
                    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))
                    {
                        // 我们不用对两个流对象进行读写,只要复制流就OK  
                        streamInput.CopyTo(outputStream);
                        outputStream.Flush();
                        isSuccessed = true;
                        Console.WriteLine("在{0}接收到客户端发送的流,已保存到test.map3。", DateTime.Now.ToLongTimeString());
                    }
                }
                catch
                {
                    isSuccessed = false;
                }
                return isSuccessed;
            }
        }
    复制代码

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

    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;
    复制代码

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

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

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

    复制代码
        /// <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 = "上传完成。";
            }
        }
    复制代码

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

    现在可以运行了。

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

                    // 接收消息的最大范围为500M  
                    //binding.MaxReceivedMessageSize = 500 * 1024 * 1024;

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

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

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

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

    复制代码
        [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))
                    {
                        // 我们不用对两个流对象进行读写,只要复制流就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 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;
            }
    复制代码

    现在再来测测吧。

    再看看服务器端。

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

  • 相关阅读:
    linux里终端安转视频播放器的操作及显示
    String字符串操作
    普通类 抽象类 接口
    java基础
    关于window的端口查看及tomcat的端口修改问题
    eclipse的应用和整理
    mysql学习
    echarts的使用
    Failed to read candidate component class
    oracle学习笔记2
  • 原文地址:https://www.cnblogs.com/ywsoftware/p/3663872.html
Copyright © 2020-2023  润新知