基于 webService 的文件传输

基于 webService 的文件传输
江苏省南通中学 吴晓春
[ 摘要 ] 用C#实现基于WebService 的文件传输程序,Web 服务的创建和调用,
并WSE 3.0、后台线程的创建模式、文件哈希校验等。
[关键词] Web 服务、后台线程、哈希校验
一、引言
由于 webService 的跨平台、分布式应用等特点,应用越来越广泛。XML 是Web Service
平台中表示数据的基本格式,它创建了一种用于表示数据、表达数据关系以及与数据进行交
互的业界标准方法。但在很多情况下将非 XML 数据发送到 Web 服务可能更为重要。例如,
将二进制数据(如 JPEG 图像)发送问题。微软的WSE(Web Service Enhancements),为程
序开发提供发这方面的方案。在WSE2.0 中,使用DIME 规范来打包数据作为SOAP 附件来
传输二进制数据。在WSE3.0 中提供了更为简洁的传输方法,本文详细这个方法的应用。
WSE3.0 作为.NET 2.0 的一个插件提供, 使用本文例程之前, 请到
http://msdn.microsoft.com/webservices/webservices/building/wse/下载安装,在VS2005 解决方
案资源管理窗口中在你的解决方案上的快捷菜单上会出现最下一行“WSE settings 3.0”。WSE
3.0 为web 服务增加了高级服务功能,并首次提供了MTOM,即信息传输优化机制,利用它
就可以快速安全地传输二进制数据。请在WSE settings3.0 对话框中,messings 标签中的配
置clientMode 或 serverMode,并在app.config 或web.config 文件中可以看到相关的配置信
息。
与 asp.net 传输文件相比,利用MTOM技术可以跨越IIS 在传输文件时MaxRequestLength
参数对文件大小的限制以及timeout 参数对时间的限制(传输过程中,没有进度提示的话,
只有当文件传输完才知道结果;或是timeout 时间到,文件传输失败。);同时MTOM 是安
全的,这是因为MTOM 对信息使用WS-规范(如WS-Security)进行全编码,所以整个信息
是安全的。当使用MTOM 支持在客户端和服务端之间传输二进制数据, WSE3.0 自动地对数
据进行编码,无需要特别的设置。
二、Web 服务开发
本文例程分为服务端和客户端。
服务端程序是基于IIS 的.net webService,可以在VS2005 中使用C#语言创建一个:
“asp.net web 服务”解决方案,方案名可以是MyWebService,并配置WSE settings 3.0,
在General 标签中选定其中仅有的两个复选框,Messaging 标签中采用默认配置。在Service.cs
中加入本文例程中相应文件的内容即可。
在 Service.cs 中提供了两个主要的方法,AppendChunk 和DownloadChunk 分别完成文件
上传和文件下载。这两个方法通过传送字节数组,来分批传送文件。AppendChunk(string
FileName, byte[] buffer, long Offset, int BytesRead)方法有四个参数,分别为文件名、
字节数组、偏移位置、字节数组长度(本次读写字节数);以追加方式打开FileName 文件,
测试文件长度与Offset 相一致时,把buffer 内容追加到文件后。若文件长度与Offset 不
一致时,说明文件传输出错,抛出异常。代码如下:
#region 文件上传
// 添加一块字节到文件
// <param name="FileName">文件名,向其中追加内容</param>
// <param name="buffer">字节数组</param>
// <param name="Offset">目前为止的文件大小</param>
// <param name="BytesRead">实际数据大小</param>
[WebMethod]
public void AppendChunk(string FileName, byte[] buffer, long Offset, int
BytesRead)
{ string FilePath = UploadPath + "\\" + FileName;
bool FileExists = File.Exists(FilePath); //确保文件已经存在
if (!FileExists || (File.Exists(FilePath) && Offset == 0))
File.Create(FilePath).Close();//若文件不存在则创建
long FileSize = new FileInfo(FilePath).Length;
if (FileSize != Offset) // 文件大小不一致,抛出异常。
CustomSoapException("传送有误", String.Format("文件大小 {0}, 预期应
为 {1} bytes", FileSize, Offset));
else
{ using (FileStream fs = new FileStream(FilePath, FileMode.Append))
fs.Write(buffer, 0, BytesRead); // 大小一致,追加到文件
}
}
#endregion
注意:本文例程中使用了语句 using(new object){ … },它作用是在using 语句后
定义对象,这个对象作用域范围是随后的大括号,若只有一行,大括号可以省略。可见,在
using 中定义的对象的作用范围比局部变量还小,有利于系统内存有效利用。
DownloadChunk 方法的声明为public byte[] DownloadChunk(string FileName, long
Offset, int BufferSize),它从文件FileName 的Offset 位置读取BufferSize 长度数据,
通过数组返回。
#region 文件下载
// 从服务器upload文件夹下载文件的一块字节
// <param name="FileName">下载文件名</param>
// <param name="Offset">下载数据的偏移位置</param>
// <param name="BufferSize">块大小</param>
// <returns>返回byte[]</returns>
[WebMethod]
public byte[] DownloadChunk(string FileName, long Offset, int BufferSize)
{ string FilePath = UploadPath + "\\" + FileName;
if (!File.Exists(FilePath)) // 文件是否存在
CustomSoapException("文件不存在", "文件 " + FilePath + " 不存在");
long FileSize = new FileInfo(FilePath).Length;
if (Offset > FileSize) // 要求下载的数据块的偏移大于文件大小,则出错
CustomSoapException("错误的下载偏移位置", String.Format("文件大小
{0}, 要求下载的位置 {1}", FileSize, Offset));
// 打开文件,读入数据到TmpBuffer[]
byte[] TmpBuffer;
int BytesRead;
using (FileStream fs = new FileStream(FilePath, FileMode.Open,
FileAccess.Read))
{ fs.Seek(Offset, SeekOrigin.Begin);
TmpBuffer = new byte[BufferSize];
BytesRead = fs.Read(TmpBuffer, 0, BufferSize);
}
if (BytesRead != BufferSize)
{ // 最后一块数据可能小于数组大小, 返回实际大小
byte[] TrimmedBuffer = new byte[BytesRead];
Array.Copy(TmpBuffer, TrimmedBuffer, BytesRead);
return TrimmedBuffer;
}
else
return TmpBuffer;
}
在Service.cs 中还提供了三个的方法,分别为
public long GetFileSize(string FileName) 根据文件名,获取其大小。
public List<string> GetFilesList() 获取文件列表,以供客户端选择下载。
public string CheckFileHash(string FileName) 计算文件的哈希值,实现文件校验。
public static void CustomSoapException() 抛出异常
public void Ping() 哑方法,用于测试连接是否成功
文件校验,是验证客户端文件与服务端文件是否一致。不可能逐一比较每个字节,可以
的校验方法有CRC 校验和哈希校验等。本文通过计算两端文件的哈希值是否一致,来验证文
件传输无误。
Web 服务的其它代码请参见源程序。接着,请在VS2005 中执行“生成网站”命令来编
译你的代码,编译通过后,执行“发布网站”。如图1 示:
图 1 网站发布
发布完成后,在浏览器中输入你发布的URL 地址(如http://localhost:8080/service.asmx)
可以看到web 服务提供的几个接口函数,在浏览器中点击调用一下,以测试web 服务。
三、调用Web 服务
接下来,创建客户端程序调用Web 服务提供的功能。重新启动VS2005,新建winClient
解决方案,添加WEB 引用,在对话框中URL 地址栏输入你的web 服务的UTL(如
http://localhost:8080/service.asmx),点击“前往”按钮,成功的话会在对话框的列表
框中会显示相应的服务接口,在web 引用名可以取myWebService。web 引用添加成功后,会
在你的解决方案下自动创建文件夹“ web refrences ” 及子文件夹
“myWebService”,myWebService 子文件夹下的内容,是VS2005 自动产生的文件,其作用
可称之谓“代理”,我们可以使用myWebService.Service 的形式调用web 服务提供的Service
类,就象调用本地的类一样;而实际工作就由“代理”去与web 服务打交道,程序员就可专
注自己的事务。然后,为客户端winClient 解决方案配置“WSE 3.0”,在对话框中Genral
标签中选定第一个复选框,在Messaging 标签中,ClientMode 选定“ON”,其它默认。
接着谈谈客户端程序的总体结构,如图2 所示。大凡窗体程序,如果执行了一个的非
常冗长的处理操作(比如文件搜索),它在执行时会锁定用户界面,虽然主活动窗口一直在
运行,但用户无法与程序交互,无法移动窗体或改变窗体大小,所以用户感觉很不爽。如何
做才能使得这个程序有响应,答案就是在后台线程中执行这个操作。已经有三种方法来创建
线程,完成工作:一是委托异步调用,将具体耗时的操作作为一个委托,并用BeginInvoke
来异步执行这个委托(Invoke 是同步调用),并且可以为这个操作传入参数并且通过
EndInvoke 方法获得返回返回值;二是使用ThreadPool,创建.net FrameWork 中的
WaitCallback 委托, 然后放到线程池中运行
ThreadPool.QueueUserWorkItem(callback),根据WaitCallback 委托的定义,可以
传入一个object 类型的参数,但是不能精确的控制线程池中的线程;三是使用Thread,
使用Thread 类可
以显式管理线程。
在.net 2.0 中,提
供了一个新的委托
ParameterizedT
hreadStart 支持
启动一个线程并传
入参数,这是对原
来的ThreadStart
委托的改进。
本文使用的是
BackgroundWor
ker 类,它也是一
个在.net 2.0 中新
增的类,可以用于
启动后台线程。同
样的功能使用
BackgroundWorker 的话会更加的简便快捷,可以节省开发时间,并把你从创建自己的委
托以及对它们的调用中解救出来。
线程结束
更新下载进度
启动线程
启动线程
计算哈希线程
窗体线程
下载线程
下载结束
窗体线程等待
图 2 客户端程序运行状态示意图
后台线程的任务如何安排呢?有两种方法,其一是继承BackgroundWorker 类,重
写BackgroundWorker.OnDoWork 方法,在其中安排后台线程要执行的代码。它实际上
就是BackgroundWorker.DoWork 事件的事件处理程序。其二是为
BackgroundWorker.DoWork 事件注册一个事件处理程序即自定义事件处理程序,在其
中添加代码,安排耗时冗长的处理操作。当事件处理程序安排好后,在窗体中调用
BackgroundWorker.RunWorkerAsync 方法,它的作用就是启动后台线程,并触发该事
件DoWork,于是事件处理程OnDoWork 方法或自定义事件处理方法就在后台线程中处
理耗时操作了。要注意,当OnDoWork 事件处理方法与自定义事件方法同时使用时,自定
义事件方法不会执行。
当然,如果需要中途中止后台线程,可以执行BackgroundWorker.CancelAsync 方
法,前提是BackgroundWorker.WorkerSupportsCancellation 属性为true;无论是中
途退出还是后台线程运行结束,都会触发当RunWorkerCompleted 事件,通过事件参数
DoWorkEventArgs.Cancel 属性是否为true,可以让事件处理程序知道是正常退出还是
中途退出。
当 后台线程执行冗长的操作时要向用户不断反馈进度, 可以调用方法
ReportProgress(int percent),它会触发ProgressChanged 事件并提供一个在 0 到
100 之间的整数来表示后台活动已完成的百分比。在窗体中通过ProgressChanged 事件
处理程序来用该整数来更新进度条。
本文例程中,实现上传和下载功能都用后台线程来完成。从BackgroundWorker 派
生出FileTransferBase,然后再从FileTransferBase 派生出FileTransferUpload、
FileTranferDownload 两个类。先来看看FileTransferBase 类的主要内容:
public class FileTransferBase : BackgroundWorker // 从BackgroundWorker派生并封装
web服务对象
{ protected mywebservice.Service ws; // web service 对象
public bool AutoSetChunkSize = true;
// 测试前5个传输块的平均大小,重新定义块大小,来适应网络带宽,提高传输
效率。
public int ChunkSize = 16 * 1024; // 默认块大小 16k
public int MaxRetries = 50; // 网络异常后,重试连接的次数
protected int NumRetries = 0; //上面50次循环的循环变量
public int AverageSample = 5; // 测试前5个传输块的平均大小
public bool IncludeHashVerification = true;
//是否要校验文件:检测本地文件的hash数据与服务端文件对照,以校验文件传
输是否正确。
public int PreferredTransferDuration = 1500; // 每块数据传输所用时间
的毫秒数
protected string LocalFileName;
protected DateTime StartTime;
protected Thread HashThread; //计算文件哈希数据的线程
public string LocalFileHash;
// 本地文件的哈希值,在文件传输完成后计算完
public string RemoteFileHash; // 服务端文件的哈希值
public string LocalFilePath; // 本地文件路径,在传输文件之前就应设
置好。
public FileTransferBase()
{ base.WorkerReportsProgress = true; //允许ReportsProgress引发进度更新
base.WorkerSupportsCancellation = true; //允许中止后台线程
}
public mywebservice.Service WebService
//封装webService对象,以便在本类内部OnDoWork方法中完成对webService调用。
protected override void OnRunWorkerCompleted(RunWorkerCompletedEventArgs
e)
{//当OnDoWork方法执行完成后,引发RunWorkerCompleted事件,执行本方法,
if(this.HashThread != null && this.HashThread.IsAlive)
this.HashThread.Abort();
base.OnRunWorkerCompleted(e);
}
protected override void OnDoWork(DoWorkEventArgs e)
{ this.WebService.Ping();// 确认连接到web服务
base.OnDoWork(e);
}
// 字节单位的描述如下:
// 1024 ="1 Kb"
// 1230000 ="1.23 Mb" MB、GB仅保留2 小数
// 若返回0 Kb, 就作为"1 Kb" ,windows 系统如此。
public static string GetFileSize(long numBytes) //格式化文件小
{ string fileSize = "";
if(numBytes > 1073741824)
fileSize = String.Format("{0:0.00} Gb", (double)numBytes / 1073741824);
else if(numBytes > 1048576)
fileSize = String.Format("{0:0.00} Mb", (double)numBytes / 1048576);
else
fileSize = String.Format("{0:0} Kb", (double)numBytes / 1024);
if(fileSize == "0 Kb")
fileSize = "1 Kb";
return fileSize;
}
// 计算文件的哈希值
protected void CheckLocalFileHash()
{
SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider();
byte[] hash;
using(FileStream fs = new FileStream(this.LocalFilePath,
FileMode.Open, FileAccess.Read, FileShare.Read, 4096))
hash = sha1.ComputeHash(fs);
this.LocalFileHash = BitConverter.ToString(hash);
}
}
FileTransferUpload、FileTranferDownload 两个类从FileTransferBase 派生并覆
盖重写OnDoWork 方法来实现上传和下载。在窗体中定义FileTransferUpload1、
FileTranferDownload2 对象,并调用这两个对象的RunWorkerAsync 方法,就能启动
后台线程并自动调用OnDoWork 方法实现上传或下载了。在上传和下载过程中,采用数据
分块的方法可有效地解决大文件传输问题,为了有效适应利用网络带宽,在传输前5 个数
据块时计算一下平均速度,重试定义数据块的大小,然后继续传输。
具体代码如下:
public class FileTransferDownload : FileTransferBase
{ public string RemoteFileName;
public string LocalSaveFolder;
//下列方法开始下载进程,并支持中止、报告进度、抛出异常
protected override void OnDoWork(DoWorkEventArgs e)
{ base.OnDoWork(e); //调用基类FileTransferBase的方法,连接到
WebService。
int numIterations = 0;//文件块个数
this.StartTime = DateTime.Now; // 用于计算前5个数据块传输的起始时

this.LocalFilePath = this.LocalSaveFolder.TrimEnd('\\') + "\\" +
RemoteFileName;
if(File.Exists(this.LocalFilePath)) // 若文件存在,覆盖建立空文件
File.Create(this.LocalFilePath).Close();
long FileSize = this.WebService.GetFileSize(this.RemoteFileName);
// 在下载之前,获取文件大小,并在界面显示
string FileSizeDescription = GetFileSize(FileSize);
//格式化文件大小 ,如把24000000000000000 字节转成"2.4 Gb"等
long ReceivedBytes = 0; // 目前已经收到的字节总数
using(FileStream fs = new FileStream(LocalFilePath, FileMode.Append,
FileAccess.Write)) //在较小的作用域内,创建本地文件流对象
{// 逐块地从web服务下载数据,当所有的字节块读完,文件就下载完成。
while(ReceivedBytes < FileSize && !this.CancellationPending)
{ // 当调用CancelAsync方法取消下载线程时,
//this.CancellationPending为真,本过程中止
if(this.AutoSetChunkSize && numIterations == AverageSample)
// 计算前5个数据块传输的平均带宽
{long timeForInitialChunks =
(long)DateTime.Now.Subtract(StartTime).TotalMilliseconds;
long averageChunkTime = Math.Max(1, timeForInitialChunks /
AverageSample);
// 平均每块用时
this.ChunkSize = (int)Math.Min(4000000, this.ChunkSize *
PreferredTransferDuration / averageChunkTime);
// 设置数据块的大小,但不超过MB。
}// if 结束
try
{
// 下载数据,写入本地文件
byte[] Buffer = this.WebService.DownloadChunk(this.RemoteFileName,
ReceivedBytes, ChunkSize);
fs.Write(Buffer, 0, Buffer.Length);
ReceivedBytes += Buffer.Length;
}
catch(Exception ex)
{
Debug.WriteLine("异常: " + ex.ToString());
if(NumRetries++ < MaxRetries)
{// 不断尝试50次,异常情况是否已恢复。}
else
{
fs.Close();//异常没有恢复,下载过程异常中断。
throw new Exception(String.Format("在下载文件过程中{0}, 请修复。
", ex.Message));
}
}
// 更新进度条
string SummaryText = String.Format("已经传输{0} / {1}",
GetFileSize(ReceivedBytes), FileSizeDescription);
this.ReportProgress((int)(((decimal)ReceivedBytes /
(decimal)FileSize) * 100), SummaryText);
numIterations++;
}// while 结束
}// using 结束
// 下载得到的文件与服务文件进行哈希校验。
if(this.IncludeHashVerification)
{ this.ReportProgress(99, "哈希校验...");
// 启动线程来计算本地哈希值
this.HashThread = new Thread(new
ThreadStart(this.CheckLocalFileHash));
this.HashThread.Start();
// 获取WEB服务的文件哈希值
this.RemoteFileHash =
this.WebService.CheckFileHash(this.RemoteFileName);
this.HashThread.Join();// 服务文件的哈希值在当前线程中计算,本地
文件哈希值在线程this.HashThread中计算,要等该纯种线程结束,当前线程才能继续。
if(this.LocalFileHash == RemoteFileHash)
e.Result = String.Format("哈希校验正确\n本地哈希: {0}\n服务
端哈希: {1}", LocalFileHash, RemoteFileHash);
else
e.Result = String.Format("哈希校验不通过\n本地哈希: {0}\n
服务端哈希: {1}", LocalFileHash, RemoteFileHash);
}//if 结束
}//OnDoWork 结束
}// FileTransferDownload 类结束
public class FileTransferUpload : FileTransferBase
{
// 下面方法启动上传过程
protected override void OnDoWork(DoWorkEventArgs e)
{ base.OnDoWork(e); //调用基类FileTransferBase的方法,连接到
WebService。
int numIterations = 0;
this.LocalFileName = Path.GetFileName(this.LocalFilePath);
if(this.AutoSetChunkSize)this.ChunkSize = 16 * 1024;//初始块大小KB
if(!File.Exists(LocalFilePath))
throw new Exception(String.Format("文件不存在:{0}",
LocalFilePath));
long FileSize = new FileInfo(LocalFilePath).Length;
string FileSizeDescription = GetFileSize(FileSize);//240000000000000
->"2.4 Gb"
long SentBytes = 0; // 目前已经上传的数据量
byte[] Buffer = new byte[ChunkSize]; // 数据块
using(FileStream fs = new FileStream(this.LocalFilePath,
FileMode.Open, FileAccess.Read))
{ int BytesRead = fs.Read(Buffer, 0, ChunkSize); // 读取第一个数
据块
// 逐块上传数据,当FileStream.Read() 返回0时, 文件上传结束。
while(BytesRead > 0 && !this.CancellationPending)
{ try{
// 上传数据块
this.WebService.AppendChunk(this.LocalFileName, Buffer,
SentBytes, BytesRead);
if(numIterations == 1) //
this.StartTime = DateTime.Now;
// 计算2~6共5个数据块的传输带宽,因第一个数据块的时间,
包含了初始化时的延迟。
if(this.AutoSetChunkSize && numIterations == AverageSample+1)
// 计算前5个数据块的平度带宽
{ long timeForInitialChunks =
(long)DateTime.Now.Subtract(StartTime).TotalMilliseconds;
long averageChunkTime = Math.Max(1,
timeForInitialChunks / AverageSample); // 平均时间,毫秒
this.ChunkSize = (int)Math.Min(4000000, this.ChunkSize *
PreferredTransferDuration / averageChunkTime); //调整块大小,不超过4MB
Buffer = new byte[ChunkSize]; //重设块的大小
}//if 结束
// sentBytes 已上传成功的字节数,当异常时可以利用它重新
上传数据
SentBytes += BytesRead;
// 更新界面
string SummaryText = String.Format("已经传送{0} / {1}",
GetFileSize(SentBytes), FileSizeDescription);
this.ReportProgress((int)(((decimal)SentBytes /
(decimal)FileSize) * 100), SummaryText);
}
catch(Exception ex)
{
Debug.WriteLine("Exception: " + ex.ToString());
if(NumRetries++ < MaxRetries)
{ // 异常发生时,回到前一数据块位置,重新发送
fs.Position -= BytesRead;
}
else
{
fs.Close();
throw new Exception(String.Format("上传数据时,异常
发生\n{0}", ex.ToString()));
}
}
BytesRead = fs.Read(Buffer, 0, ChunkSize);// 读取本地文件下
一个数据块
numIterations++;//数据块计数
}// while 结束
}//using 结束
if(this.IncludeHashVerification)
{ //文件哈希校验,代码参见源程序 }
}//OnDoWork 结束
}// FileTransferUpload 结束
在下载之前,程序必须提供服务端文件列表,这个功能也用到了BackGroundWork
类,与上传下载不同的是,直接定义BackGroundWork 类对象workerGetFileList,为
workerGetFileList.DoWork 事件注册事件处理方法workerGetFileList_DoWork,由该
方法获取服务端文件列表。workerGetFileList 对象演示了BackGroundWork 类的另一
种使用方法。代码请参见源程序。
当上传或下载完成之后,客户端要求服务端返回文件的哈希值与本地文件比较判断文件
是否一致。当文件比较大时,比如100MB 以上,服务端计算并返回哈希值时要好几秒钟,
本地计算也要这么长时间。为了缩短时间,可用多线程让它们同时计算,当前线程获取服务
端文件的哈希值的同时,创建一个线程计算本地文件哈希值,并用Join()方法等线程完成
计算后再比较是否一致。
this.ReportProgress(99, "哈希校验...");
this.HashThread = new Thread(new ThreadStart(this.CheckLocalFileHash)); // 计算本地哈希
this.HashThread.Start();
this.RemoteFileHash = this.WebService.CheckFileHash(this.RemoteFileName); // 获取WEB服务的文
件哈希
this.HashThread.Join();// 当前线程暂停,等待本地文件计算结束
if(this.LocalFileHash == RemoteFileHash)
e.Result ="哈希校验正确";
else
e.Result ="哈希校验不通过";
所以,例程中除了窗体主线程外共有四处使用线程,一个下载线程、一个上传线程、获
取文件列表线程、计算文件哈希值线程。
四、结语
本文上半部分介绍了基于webService的二进制文件传输,使用WSE 3.0来优化速度、
加强安全。下部半部分介绍了利用BackGroundWork类开发窗体程序的设计模式。在调试
本文例程时,要注意客户端引用webService时与服务端程序发布的URL是否一致,当服务
端程序调试时要重新发布web服务,客户端web引用要随之更新。并请在WEB服务的URL
根下创建upload文件夹存放上传文件,同时也是提供下载的文件夹。运行环境为WIN2003
及IIS 6.0,VS2005,WSE 3.0。
posted on 2011-09-22 12:19  Kingly  阅读(4523)  评论(0)    收藏  举报