大电子文件读取成二进制流方案

最近开发的时候遇到用户提到的BT需求,泥马要把上G的电子文件导入到系统数据库中,这不是坑爹吗?还天天发邮件打电话来催,没办法,用户就是上帝!我们这帮苦逼的程序猿也得照样着,以下就说下这几天的研究过程吧!

问题出现的背景:

以前上传电子文件在读取文件的时候,遇到大电子文件的时候就会时不时给你来个OutOfMemoryException这坑爹的异常,问了下度娘原因是多种多样的!有涉及到修改服务器的配置啊什么的,服务器咱也动不了,这类方案就没尝试了。所以只有从自己的代码上做文章了!

原来的代码如下:

View Code
#region 根据文件的完整路径获取二进制文件(old读取大文件的时候会报异常)
        /// <summary>
        /// 从指定的文件路径中读取文件,返回文件二进制数据
        /// </summary>
        /// <param name="FileName">文件名称</param>
        /// <param name="FilePath">文件的完整路径</param>
        public byte[] ReadFileOld(string FileName, string FilePath)
        {
            try
            {
                //创建文件流
                FileStream fsReader = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                MemoryStream mem = new MemoryStream();
                byte[] buffer = new byte[1024];
                int bytesRead = 0;
                int TotalByteRead = 0;
                while (true)
                {
                    bytesRead = fsReader.Read(buffer, 0, buffer.Length);
                    TotalByteRead += bytesRead;
                    if (bytesRead == 0)
                        break;
                    mem.Write(buffer, 0, bytesRead);
                }
                if (mem.Length > 0)
                {
                    byte[] bytes=mem.ToArray();
                    fsReader.Close();
                    fsReader.Dispose();
                    mem.Dispose();
                    mem.Close();
                    return bytes;
                }
                else
                {
                    return null;
                }
            }
            catch (Exception ep)
            {
                throw ep;
            }
        }
        #endregion

此方法读取电子文件的时候,文件过大就会出现OutOfMemoryException异常,分析发现罪魁祸首就是MemoryStream,该流是牺牲内存来读取文件的,当计算机的内存被占用到一定的数量的时候就会出现该异常!

解决方案:摒弃内存相关的流,就出现如下的方法

View Code
#region 根据文件的完整路径获取二进制文件(测试用,也可上传大点的电子文件)
        /// <summary>
        /// 从指定的文件路径中读取文件,返回文件二进制数据
        /// </summary>
        /// <param name="FileName">文件名称</param>
        /// <param name="FilePath">文件的完整路径</param>
        public byte[] ReadFileTest(string FileName, string FilePath)
        {
            try
            {
                //创建文件流
                FileStream Reader = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

                // 创建一个二进制数据流读入器,和打开的文件关联
                BinaryReader brMyfile = new BinaryReader(Reader);
                // 把文件指针重新定位到文件的开始
                brMyfile.BaseStream.Seek(0, SeekOrigin.Begin);
                byte[] bytes = brMyfile.ReadBytes(Convert.ToInt32(Reader.Length.ToString()));
                // 关闭以上new的各个对象
                brMyfile.Close();
                Reader.Dispose();//释放内存
                return bytes;
            }
            catch (Exception ep)
            {
                throw ep;
            }
        }
        #endregion

这个方法测试了几次,大点的电子文件上传不会出现内存溢出异常了,但是具体能上传多大的电子文件我也没多测试就测试了一个600M多点的电子文件!

最终使用的方案:

查询网上各位大牛的方案,出现大电子文件分块读取,这是一个不错的思想,不过现在项目中需要的是一次返回byte[]数组,和分块读取显示有一定的差距!所以还是自己动手来写个方法吧,具体代码如下:

View Code
 #region 根据文件的完整路径获取二进制文件
        /// <summary>
        /// 从指定的文件路径中读取文件,返回文件二进制数据
        /// </summary>
        /// <param name="FileName">文件名称</param>
        /// <param name="FilePath">文件的完整路径</param>
        public byte[] ReadFile(string FileName, string FilePath)
        {
            FileStream fsReader = null;
            try
            {
                //创建文件流
                fsReader = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

                int bufferSize = 1024;//每次读取的字节数
                byte[] buffer = new byte[bufferSize];//缓冲区数组
                long fileLength = fsReader.Length;//文件流的字节总长度
                int readCount = (int)Math.Ceiling(((double)fileLength / (double)bufferSize)); //需要对文件读取的次数
                int currentReadCount = 0;//当前读取次数
                byte[] totalBytes = new byte[fileLength];//用来保存文件的字节数组
                long bytesIndex = 0;
                //最后一次循环需要读取的字节长度,用来初始化数组,避免读取到空值的情况
                long lastReadLength = fileLength - (readCount - 1) * bufferSize;

                while (currentReadCount < readCount)
                {
                    //分readCount次读取这个文件流,每次从上次读取的结束位置开始读取bufferSize个字节
                    long position = currentReadCount * bufferSize;
                    fsReader.Seek(position, SeekOrigin.Begin);//设置当前流的读取起始位置
                    if (currentReadCount == (readCount - 1))//最后一次
                    {
                        byte[] lastBuffer = new byte[lastReadLength];
                        bytesIndex = fsReader.Read(lastBuffer, 0, (int)lastReadLength);//读取最后一部分剩余字节
                        lastBuffer.CopyTo(totalBytes, position);//加入到需要返回的字节数组中去
                    }
                    else
                    {
                        bytesIndex = fsReader.Read(buffer, 0, bufferSize);
                        buffer.CopyTo(totalBytes, position);//加入到需要返回的字节数组中去
                    }
                    if (bytesIndex == 0)//读取完成
                    {
                        break;
                    }

                    currentReadCount++;//读取完成后当前读取次数加1

                }
                fsReader.Dispose();
                fsReader.Close();
                return totalBytes;
            }
            catch (Exception ep)
            {
                if (fsReader != null)
                {
                    fsReader.Dispose();
                }
                throw ep;
            }
            finally
            {
                if (fsReader != null)
                {
                    fsReader.Dispose();
                }
            }
        }
        #endregion

用这个方法做了一个cs模式的导入工具给用户来导入电子文件,大的电子文件导入没太大问题了!不过没试过太大的!

别以为这样就完了,尼玛的用户提了一个需求涉及到从FTP上读取大电子文件并保存到数据库中,没办法只好继续研究!

FTP读取下载一般文件的类我就不提了,问下度娘就能找到FtpHelper类,我主要说下自己写的读取大文件的类吧,有了之前CS模式读取大电子文件的经验,心中暗自高兴!以前的研究终于用上了啊!于是Copy过来修改了一下读取流的方式,代码如下:

View Code
/// <summary>
        /// 从FTP服务器下载文件,返回文件二进制数据
        /// </summary>
        /// <param name="RemoteFileName">远程文件名</param>
        public byte[] DownloadFileOld(string RemoteFileName)
        {
            Stream fsReader = null;
            try
            {
                if (!IsValidFileChars(RemoteFileName))
                {
                    throw new Exception("Invalid File Name or Directory Name!");
                }
                Response = Open(new Uri(this.Uri.ToString() + RemoteFileName), WebRequestMethods.Ftp.DownloadFile);
                fsReader = Response.GetResponseStream();

                int bufferSize = 1024;//每次读取的字节数
                byte[] buffer = new byte[bufferSize];//缓冲区数组
                long fileLength = fsReader.Length;//文件流的字节总长度
                int readCount = (int)Math.Ceiling(((double)fileLength / (double)bufferSize)); //需要对文件读取的次数
                int currentReadCount = 0;//当前读取次数
                byte[] totalBytes = new byte[fileLength];//用来保存文件的字节数组
                long bytesIndex = 0;
                //最后一次循环需要读取的字节长度,用来初始化数组,避免读取到空值的情况
                long lastReadLength = fileLength - (readCount - 1) * bufferSize;

                while (currentReadCount < readCount)
                {
                    //分readCount次读取这个文件流,每次从上次读取的结束位置开始读取bufferSize个字节
                    long position = currentReadCount * bufferSize;
                    fsReader.Seek(position, SeekOrigin.Begin);//设置当前流的读取起始位置
                    if (currentReadCount == (readCount - 1))//最后一次
                    {
                        byte[] lastBuffer = new byte[lastReadLength];
                        bytesIndex = fsReader.Read(lastBuffer, 0, (int)lastReadLength);//读取最后一部分剩余字节
                        lastBuffer.CopyTo(totalBytes, position);//加入到需要返回的字节数组中去
                    }
                    else
                    {
                        bytesIndex = fsReader.Read(buffer, 0, bufferSize);
                        buffer.CopyTo(totalBytes, position);//加入到需要返回的字节数组中去
                    }
                    if (bytesIndex == 0)//读取完成
                    {
                        break;
                    }

                    currentReadCount++;//读取完成后当前读取次数加1

                }
                fsReader.Dispose();
                fsReader.Close();
                return totalBytes;

            }
            catch (Exception ep)
            {
                if (fsReader != null)
                {
                    fsReader.Dispose();
                }
                ErrorMsg = ep.ToString();
                throw ep;
            }
            finally
            {
                if (fsReader != null)
                {
                    fsReader.Dispose();
                }
            }
        }

尼玛一测试报一个该流不支持查找的操作,读取FTP上的文件是通过NetworkStream流来反应的,和读取文件流不一样,上MSDN上一查,坑爹!NetworkStream为了安全不支持seek等查找方法,以下方法就不能使用了!

fsReader.Seek(position, SeekOrigin.Begin);//设置当前流的读取起始位置

和查找相关的方法啊属性都不能使用了,看来只有自己想办法来读取NetworkStream中的数据了,经过测试最终实现了方法如下:

View Code
/// <summary>
        /// 从FTP服务器下载文件,返回文件二进制数据
        /// </summary>
        /// <param name="RemoteFileName">远程文件名</param>
        public byte[] DownloadFile(string RemoteFileName)
        {
            Stream Reader=null;
            try
            {
                if (!IsValidFileChars(RemoteFileName))
                {
                    throw new Exception("Invalid File Name or Directory Name!");
                }
                Response = Open(new Uri(this.Uri.ToString() + RemoteFileName), WebRequestMethods.Ftp.DownloadFile);
                Reader = Response.GetResponseStream();

                int bufferSize = 1024;//每次读取的字节数
                byte[] buffer;//缓存区数组
                int bytesRead = 0;
                long TotalByteRead = 0;//记录文件的总的字节数
                List<byte[]> listBytes = new List<byte[]>();//记录每次读取的byte数组
                while (true)
                {
                    buffer = new byte[bufferSize];//缓冲区数组
                    bytesRead = Reader.Read(buffer, 0, buffer.Length);
                    TotalByteRead += bytesRead;
                    if ((bytesRead < bufferSize) && (bytesRead != 0))//读取到最后一次了
                    {
                        //Reader.Seek((TotalByteRead - bytesRead),SeekOrigin.Begin);//NetworkStream流不支持查找功能
                        MemoryStream mem = new MemoryStream();
                        mem.Write(buffer, 0, bytesRead);
                        buffer = new byte[bytesRead];
                        buffer = mem.ToArray();
                        mem.Dispose();
                    }

                    if (bytesRead == 0)
                    {
                        break;
                    }
                    else
                    {
                        listBytes.Add(buffer);
                        buffer = null;
                    }

                }
                //定义一个字节数组来保存需要返回的二进制数据
                byte[] totalBytes = new byte[TotalByteRead];
                long startIndex = 0;//元素复制的索引起始值
                for (int i = 0; i < listBytes.Count; i++)
                {
                    if (i == 0)
                    {
                        startIndex = 0;
                    }
                    else
                    {
                        startIndex += listBytes[i - 1].Length;
                    }

                    listBytes[i].CopyTo(totalBytes, startIndex);
                }

                Reader.Dispose();
                Reader.Close();
                return totalBytes;
            }
            catch (Exception ep)
            {
                ErrorMsg = ep.ToString();
                throw ep;
            }
        }

程式发布到服务器上去也能从FTP上读取大电子文件了,用户那边也有个交代了!

原文地址:https://www.cnblogs.com/StevenDu/p/doNET.html