最近开发的时候遇到用户提到的BT需求,泥马要把上G的电子文件导入到系统数据库中,这不是坑爹吗?还天天发邮件打电话来催,没办法,用户就是上帝!我们这帮苦逼的程序猿也得照样着,以下就说下这几天的研究过程吧!
问题出现的背景:
以前上传电子文件在读取文件的时候,遇到大电子文件的时候就会时不时给你来个OutOfMemoryException这坑爹的异常,问了下度娘原因是多种多样的!有涉及到修改服务器的配置啊什么的,服务器咱也动不了,这类方案就没尝试了。所以只有从自己的代码上做文章了!
原来的代码如下:

#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,该流是牺牲内存来读取文件的,当计算机的内存被占用到一定的数量的时候就会出现该异常!
解决方案:摒弃内存相关的流,就出现如下的方法

#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[]数组,和分块读取显示有一定的差距!所以还是自己动手来写个方法吧,具体代码如下:

#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过来修改了一下读取流的方式,代码如下:

/// <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中的数据了,经过测试最终实现了方法如下:

/// <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上读取大电子文件了,用户那边也有个交代了!