FunctionGHW

立志成为真正程序员的码农,伪全栈,目前主要写C#

WCF流模式(Streamed)传输文件之遇到文件被其他进程占用的问题

FunctionGHW posted @ 2014年11月02日 22:24 in C#与.Net with tags dispose WCF Streamed 文件传输 IDisposable , 1958 阅读

最近在工作中,需要实现一个文件上传下载的功能,因为项目使用了WCF技术, 就尝试能否使用WCF来实现。经过一番搜索,很轻松就找到了大量资料--流模式实现文件传输。功能的实现, 网上已经有一堆资料和代码了,这里只提几个我觉得需要注意的地方,以及遇到的问题。

先看一段配置信息:

        <!-- Endpoint Binding Configuration -->
        <basicHttpBinding>
                <binding name="FileTransferHttpBinding"
                         transferMode="Streamed"
                         maxReceivedMessageSize="1000000000000"
                         closeTimeout="00:00:10"
                         openTimeout="00:00:10"
                         receiveTimeout="00:20:00"
                         sendTimeout="00:20:00">
                    <security mode="None"/>
                </binding>
            </basicHttpBinding>
        

因为传输的文件可能会很大,绑定的传输模式必须用Streamed替代默认的Buffered, 顺便提一下,只有TCP, IPC, basicHTTP 绑定支持Streamed。此外, 还需要注意maxReceivedMessageSize, receiveTimeout, sendTimeout这三个设置。 第一个设置决定了能接受多大的文件(单位byte),后两个超时设置则是保证能把文件传输完毕, 当持续传输的时间超过设置的时间后,程序会认为连接超时,自动断开,导致传输失败(失败了n次后的血的教训啊)。 这里我是随便设置的一个值,实际使用,需要考虑可能出现的最大文件大小和网络情况来取一个合理的值。 receiveTimeout作用在接收文件的一端,sendTimeout作用于发送文件的一端,因为有上传有下载, 所以两个时间我都需要设置。其他的设置项是粘贴网上的,似乎不影响现在的程序运行,先不管了, 不明白请自行查阅MSDN和Google。

因为想要附加一些额外的信息,如文件名,大小等信息, 所以没有直接使用System.IO.Stream类型作为参数和返回值,而是定义了自己的消息类。看起来像这个样子。

    // 文件请求对象,下载文件时可以作为参数使用
    [MessageContract]
    public class FileRequestObject
    {
        [MessageHeader]
        public string FileName { get; set; }

        // 省略其他属性
    }

    // 文件传输对象,Stream必须作为唯一主体
    [MessageContract]
    public class FileTransportObject
    {
        [MessageHeader]
        public string FileName { get; set; }

        [MessageHeader]
        public long FileSize { get; set; }
        
        // 省略其他属性

        [MessageBodyMember]
        public Stream FileStream { get; set; }
    }
        

服务契约的定义。

    [ServiceContract]
    public interface IFileService
    {
        [OperationContract]
        FileRequestObject UploadFile(FileTransportObject fto);

        [OperationContract]
        FileTransportObject DownloadFile(FileRequestObject request);
    }
        

具体的功能实现就是简单的Stream读写操作了,不再细说,请参考网上的代码。 最后实现的效果还可以,16GB的文件无压力(需要一杯咖啡慢慢等待)。

发现问题

本以为到此就算成功了,不过前段时间出现问题了:如果下载了某个文件,修改了再上传, 即覆盖原来的文件,那么就可能会出现文件被另一个进程占用的问题。经过一番调试发现这是文件流没有被关闭导致的, 这个流是要返回给客户端去读的。

        public FileTransportObject DownloadFile(FileRequestObject request)
        {
            // 这里不能使用using或者try-finally之类的代码去保证关闭fileStream,
            // 因为该对象是要被返回给客户端的,一旦被文件流被关闭了,连接就断了。
            // 这也就导致了问题的出现。
            ......
            var fileStream = File.OpenRead(filePath);
            long fileSize = fileStream.Length;
            FileTransportObject fto = new FileTransportObject()
            {
                FileName = fileName,
                FileSize = fileSize,
                FileStream = fileStream
                ......
            };
            ......
            return fto;
        }
        

为了解决这个问题,我们也想了几个办法:

因为项目中需要下载后修改再上传的文件不是很大, 可以用一个MemoryStream去把文件全部读到内存,然后关闭文件流, 最后把这个MemoryStream返回给客户端,这样就避开了这个文件流的关闭的问题。但是这个MemoryStream谁来释放呢? 内存开销怎么办? 这个办法显然缺点比较明显。

另一个办法是再定义一个契约操作,用来通知服务端释放资源。服务端在返回文件流的时候, 把这个文件流记录下来,并生成一个唯一标识返回给客户端,当客户端下载完成后,使用这个标识调用释放操作, 服务端再去释放这个文件流。听起来似乎好点,但是如果客户端突然跪了,没来得及调用释放操作呢?

解决方案

这些办法更像是没有办法的办法,难道微软搞了个WCF就没有考虑到这个问题?我不信。 终于在这个周末,持续不断的Google搜索后,找到了我想要的答案。 链接

在操作行为OperationBehavior里有一个属性AutoDisposeParameters:

This property determines whether the service disposes all disposable parameters (input, output or reference parameters) that were created while processing a message. The default value is true. Set this property to false if you want to prevent the system from disposing of resources and cache them if required. For example, if AutoDisposeParameters is false, then the sender is responsible for closing the stream on the sending side.
引用自:MSDN

 

当直接使用Stream作为参数或者返回值,因为AutoDisposeParameters属性的存在, 默认情况下,Stream是会被自动释放的,但是当我们自己定义个一个包含Stream主体的消息类之后, Stream是不会被自动释放的。

If you wrap the Stream in MessageContract (so you could sent more information in headers), beware that the Stream would not be disposed automatically!

 

如果你想让WCF自动的释放这个Stream,就必须让自己的消息类实现IDisposable接口, 在Dispose方法中去释放文件流(对其他的非托管资源应该也一样)。看到这也就知道, 这个自动释放资源的秘密就在于IDisposable接口了,因为Stream类也实现了该接口。

As the name of attribute OperationBehavior.AutoDisposeParameters suggests, WCF automatically disposes input/output parameters and thus you have to implement IDisposable on your MessageContract class and close the stream there

修改代码,实现IDisposable接口,问题果然解决了。

    // 实现IDisposable接口并在Dispose方法中释放文件流,
    // 配合OperationBehaviorAttribute.AutoDisposeParameters属性,
    // 保证WCF可以自动释放文件流
    [MessageContract]
    public class FileTransportObject : IDisposable
    {
        [MessageHeader]
        public string FileName { get; set; }

        [MessageHeader]
        public long FileSize { get; set; }
        
        // 省略其他属性

        [MessageBodyMember]
        public Stream FileStream { get; set; }

        public void Dispose()
        {
            if (FileStream != null)
            {
                //Console.WriteLine("disposing: {0}", FileName);
                FileStream.Dispose();
            }
        }
    }
        

 

  • 无匹配

登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter