作者:Kanasz Robert
介绍
几个月前,老板要我创建一个Outlook插件,能够将email附件保存到SQL Server数据库。主要的问题是用户没办法直接连接SQL Server,因为SQL Server在防火墙的后面。第一个版本的文件传输模块没有将文件分割成小块,整个文件传送。当用户传送大文件时可能会产生问题。我决定添加ProgressBar显示文件传送进度,这是第二个问题,如何解决呢?我想到了一个好的解决方法,将文件分成独立部分传送到服务器。于是我创建了.NET Web Service用于传送文件。本文中我将介绍它是如何工作的并提供一个简单的项目。
背景
选项之一,如何使用.NET Web Service将大文件解析为多个部分传送到服务器临时文件中,然后存储到数据库。下面是具体步骤:
1)加载需要传送的文件
2)分割成小块
3)传送小块到服务器
4)合并小块到临时文件
5)保存临时文件到数据库
6)删除临时文件
当你加载好文件并分割为小块,可以独立传送到服务器,你可以很容易对传送进行监控。例如,你想传送1MB,你可以分割为10个部分,传送好一个部分,你可以更新一下ProgressBar,我会解释如何工作的。
数据库
这里,你肯能会问,为什么我使用数据库而不是将文件放到文件夹中。将文件存储到数据库有很多理由,文件存储到数据库表中,你备份数据库的时候,文件也随之备份了。当文件不是太大的时候这是一个有效的方法,对我的解决方案而言这是个简单的方法。
现在让我们看看数据库是怎么样的,这个例子中,我创建了名为FileStorage的数据库,只有一张名为File的表。
CREATE TABLE [dbo].[File]( [FileID] [int] IDENTITY(1,1) NOT NULL, [Filename] [varchar](100) NULL, [Content] [varbinary](max) NULL, CONSTRAINT [PK_File] PRIMARY KEY CLUSTERED ( [FileID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, _ IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
如你所见,这是只有三个字段的简单数据表,主键(FileID)是int类型,Filename是varchar类型用于存储文件名,varbinary类型的字段Content用于存储文件数据。
一个主要的问题是如何从表中获取数据?为了此目的,我写了一个spFile_Add存储过程,这只是一种方式,你也可以用标准的SQL命令,具体用哪一种方式取决于你。
spFile_Add
CREATE PROCEDURE [dbo].[spFile_Add] ( @Filename varchar(100), @Content varbinary(max) ) AS BEGIN INSERT INTO [File]([Filename],[Content]) VALUES (@Filename, @Content) END
存储过程将一个新的文件插入到File表,@Filename参数varchar类型,描述了存储的文件名,第二个参数是@Content,varbinary类型,保存文件二进制内容。
创建Web Service
数据库建好后,我们可以写一个Web Service,当SQL Server(或其它资源)无法直接访问时,Web Services是非常有用的,就像我现在的情况。
使用Visual Studio 2008,我们用ASP.NET Web Service应用模板创建Web Service。这个例子中,我已经创建了了名为FileTransferWebService的项目,Web Service:FileTranser.asmx。默认有两个文件被创建:
AssemblyInfo.cs 包含了程序集的版本和配置信息
Web.config 定义应用如何运行(debug选项,cookies的使用,等等)。也包含了connectionStrings 区域, 你可以将数据库连接字符串写在这里。
本例中我创建的连接字符串名字为ftConnString:
<connectionStrings> <add name="ftConnString" connectionString="user id=@USER;pwd=@PASSWORD; data source=@DBSERVER;persist security info=False; initial catalog=FileStorage;" providerName="System.Data.SqlClient" /> </connectionStrings>
不要忘记设置一个有效的user id,pwd和data source。
每一个Web Service公开方法都有WebMethod属性。该属性写在方法的前面,便可以指定该方法为Web Service方法。在本例中,我创建了3个Web Service方法。
[WebMethod] public string GetTempFilename() { try { Guid g = Guid.NewGuid(); //This method creates a temporary file on disk and //returns name of temporary file. //In this case it will be new Guid. FileStream fs = new FileStream(Server.MapPath(g.ToString()), FileMode.Create); fs.Close(); return g.ToString(); } catch (Exception err) { throw new Exception("GetTempFilename: " + err.Message); } }
GetTempFilename()方法是你第一个调用的方法,该方法在服务器上创建了一个临时文件用于存储文件分块。
[WebMethod] public void AppendToTempFile(string tempFilename, byte[] data) { try { //This method appends block of data to the end of the temporary file. FileStream fs = new FileStream(Server.MapPath(tempFilename), FileMode.Append); BinaryWriter bw = new BinaryWriter(fs); bw.Write(data); bw.Close(); fs.Close(); } catch (Exception err) { throw new Exception("AppendToTempFile: " + err.Message); } }
该方法将文件内容添加到刚才用GetTempFilename()方法生成的文件中。tempFilename 参数指定文件名。
[WebMethod] public void SaveFileIntoDatabase(string filename, string tempFilename) { SqlDatabase sqlDatabase = new SqlDatabase (ConfigurationManager.ConnectionStrings["ftConnString"].ConnectionString); //Now we must get all bytes from file and put them into fileData byte array. //This byte array we later save on database into File table. byte[] fileData = GetBytes(tempFilename); string sql = "spFile_Add"; SqlCommand sqlCommand = sqlDatabase.GetStoredProcCommand(sql) as SqlCommand; try { sqlDatabase.AddInParameter(sqlCommand, "@Filename", SqlDbType.VarChar, filename); sqlDatabase.AddInParameter(sqlCommand, "@Content", SqlDbType.VarBinary, fileData); sqlDatabase.ExecuteNonQuery(sqlCommand); } catch (Exception err) { throw new Exception("SaveFileIntoDatabase: " + err.Message); } } public byte[] GetBytes(string path) { FileStream fs=null; BinaryReader br=null; try { byte[] buffer = null; fs = new FileStream(Server.MapPath(path), FileMode.Open, FileAccess.Read); br = new BinaryReader(fs); long numBytes = new FileInfo(Server.MapPath(path)).Length; buffer = br.ReadBytes((int)numBytes); br.Close(); fs.Close(); return buffer; } catch (Exception err) { throw new Exception("SaveFileIntoDatabase: " + err.Message); } }
SaveFileIntoDatabase(string filename, string tempFilename)方法将临时文件保存到数据库。filename参数指定原始文件名,tempFilename指定临时文件,将临时文件通过GetBytes(string path)加载到byte数组fileData,path指定了临时文件的物理路径。
通过SqlDatabase类连接数据库,在定义类实例之前,你必须包含命名空间Microsoft.Practices.EnterpriseLibrary.Data.Sql,如果你没有安装微软企业库,你可以到这里下载:http://msdn.microsoft.com/en-us/library/ff648951.aspx
你应使用SqlCommand执行存储过程spFile_Add,当文件上传成功,你可以用File.Delete()删除临时文件。
添加Web Service
当你想使用Web Service,必须首先告诉应用哪里可以找到它。Visual Studio 2008有一个很好的向导可以帮你添加Web Service的引用。
在解决方案浏览器中,右键点你的项目然后选择"添加服务引用"
然后点"高级"按钮
在"服务引用设置",点"添加Web引用"按钮。
在地址栏,输入Web Service的URL,在我的例子中,URL是:http://localhost/FileTransferWebService/FileTransfer.asmx,在添加Web 引用之前,应先写好Web引用的名称,我用FileTransferWebService这个名字,之后点击"添加引用"按钮。
使用Web Service
Web引用添加好后,可以开始写代码了。在我的例子里,我创建了一个名为frmMain的form,
拥有以下对象:
名字 描述
txtFile 存储上传文件的路径
cmdSelectFile 显示OpenFileDialog
pbTansferStatus 显示传输状态
cmdSave 开始传输操作
在将文件存入数据库之前,你需要选择文件。点击cmdSelect按钮,打开了OpenFileDialog 窗口,浏览文件并选择文件。选择好后,在txtFile文本框中可以看到文件的全路径。之后我们将通过FileStream使用该文件。FileStream类的实例名为fs,作为BinaryReader的参数,读取并存储字节到名为b的字节数组。ReadBytes函数从当前流读取指定字节数的数据到字节数组中。当我们得到了小块文件的字节数组,我们通过AppendToTempFile()方法传送到服务器。该方法由FileTransfer类提供。
private void cmdSelectFile_Click(object sender, EventArgs e) { //Using OpenFileDialog we can browse through file system //and select a file, we want to save into to database. OpenFileDialog dialog = new OpenFileDialog(); dialog.Filter = "All files (*.*)|*.*"; dialog.Title = "Select a file"; txtFile.Text= (dialog.ShowDialog() == DialogResult.OK) ? dialog.FileName : null; } private void cmdSave_Click(object sender, EventArgs e) { try { //now we have to set value of pbTransferSatatus to 0. pbTransferStatus.Value = 0; //Before saving we must create an instance of FileTransfer class, //which allows us use 3 methods mentioned earlier. FileTransferWebService.FileTransfer ft = new FileTransfer.FileTransferWebService.FileTransfer(); //GetTemFilename() method creates temporary file on server and returns it's name. string tempFileName = ft.GetTempFilename(); FileInfo fi = new FileInfo(txtFile.Text); using (FileStream fs = new FileStream(txtFile.Text,FileMode.Open,FileAccess.Read)) { //using BinaryReader we will read blocks of data from file //and append then to the temporary file. using (BinaryReader br = new BinaryReader(fs)) { fs.Position = 0; int numBytesToRead = (int)fs.Length; int numBytesRead = 0; while (numBytesToRead > 0) { //I choose 100 000 bytes long blocks. byte[] b = br.ReadBytes(100000); ft.AppendToTempFile(tempFileName, b); if (b.Length == 0) { break; } numBytesRead += b.Length; numBytesToRead -= b.Length; //When block is saved, than we can update pbTransferStatus value int progressStatus = (int)((double)100 / (double)fs.Length * (double)numBytesRead); pbTransferStatus.Value = progressStatus; } ft.SaveFileIntoDatabase(Path.GetFileName(txtFile.Text), tempFileName); MessageBox.Show("File has been saved into database", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); } } } catch (Exception err) { MessageBox.Show (err.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } }
结论
在这篇文章中,我描述了如何存储大文件到数据库表中,如何监控状态转移。 为了这个目的,我们编写了.NET Web服务。 编写Web服务是很容易的。 Visual Studio的向导使得编写services非常快。 在这个例子中,使用Web服务不是唯一的解决方案,它是一个可以选择的解决方案之一。
源代码下载:http://www.okbase.net/file/item/23081