其实在tcp/ip协议中传输文件可以保证传输的有效性,但有一个问题文件传了一部分连接意外断开了怎样;那这种情况只能在重新连接后继续传输,由于文件那部分已经传了那部分没有完成并不是tcp/ip的范围,所以需要自己来制定协议达到到这个目的。实现这个续传的协议制定其实也是非常简单,通过协议把文件按块来划分,每完成一个块就打上一个标记;即使是连接断了通过标记状态就知道还需要传那些内容。下面通过beetle来实现一个简单断点续传的程序(包括服务端和客户端)。
在实现之前先整理一下流程思路,首先提交一个发送请求信息包括(文件名,块大小,块的数量等),等对方确认后就进行文件块发送,对方接收块写入后返回一个标记,然后再继续发直到所有发送完成。思路明确后就制定协了:
文件传输申请信息
public class Post:MessageBase
{
public string FileName;
public long Size;
public int PackageSize;
public int Packages;
public Post()
{
FileID = Guid.NewGuid().ToString("N");
}
}
public class PostResponse : MessageBase
{
public string Status;
}
FileID这个值是用来协同工作的,两端根据这个ID来找到具体操作的文件和相关信息;Response提供了一个Status属性,可以用来提供一个错误的描述,如果无有任何值的情况说明对方允许这个行为.
文件块传输信息
public class PostPackage:MessageBase
{
public byte[] Data;
public int Index;
}
public class PostPackageResponse : MessageBase
{
public int Index;
public string Status;
}
文件块传输也是一个请求,一个应答;分别带的信息就是块数据信息和块的位置,同样也是根据Status信息来标记块的处理是否成功。
结构定义完成了,那就进行逻辑处理部分;不过为了调用更方便还需要封装一些东西,如根据块大小来划分文件块的数目,获取某一文件块的内容和写入文件某一些的内容等功能。
public static int GetFilePackages(long filesize)
{
int count;
if (filesize % PackageSize > 0)
{
count = Convert.ToInt32(filesize / PackageSize) + 1;
}
else
{
count = Convert.ToInt32(filesize / PackageSize);
}
return count;
}
public static byte[] FileRead(string filename, int index, int size)
{
using (Smark.Core.ObjectEnter oe = new Smark.Core.ObjectEnter(filename))
{
byte[] resutl = null;
long length = (long)index * (long)size + size;
using (System.IO.FileStream stream = System.IO.File.OpenRead(filename))
{
if (length > stream.Length)
{
resutl = new byte[stream.Length - ((long)index * (long)size)];
}
else
{
resutl = new byte[size];
}
stream.Seek((long)index * (long)size, System.IO.SeekOrigin.Begin);
stream.Read(resutl, 0, resutl.Length);
}
return resutl;
}
}
public static void FileWrite(string filename, int index, int size, byte[] data)
{
using (Smark.Core.ObjectEnter oe = new Smark.Core.ObjectEnter(filename))
{
using (System.IO.FileStream stream = System.IO.File.OpenWrite(filename))
{
stream.Seek((long)index * (long)size, System.IO.SeekOrigin.Begin);
stream.Write(data, 0, data.Length);
stream.Flush();
}
}
}
准备工作完成了,就开始写接收端的代码了。之前的文章已经介绍了Beetle如果创建一个服务和绑定分包机制,在这里就不多说了;看下接收的逻辑是怎样处理了.
接收传文件请求
public void Post(ChannelAdapter adapter, Beetle.FileTransfer.Post e)
{
string file = txtFolder.Text + e.FileName;
PostResponse response = new PostResponse();
response.FileID = e.FileID;
response.ID = e.ID;
try
{
if (FileTransferUtils.CreateFile(file, e.Size))
{
Logics.FileItem item = new Logics.FileItem();
item.FileID = e.FileID;
item.FileName = file;
item.Packages = e.Packages;
item.PackageSize = e.PackageSize;
item.Completed = 0;
item.Size = e.Size;
Logics.Access.Update(item);
AddItem(item);
}
else
{
response.Status = "不能创建文件!";
}
}
catch (Exception e_)
{
response.Status = e_.Message;
}
adapter.Send(response);
}
接收请求后根据信息创建临时文件,创建成功就把文件相关信息保存到数据库中,如果失败或处理异常就设置相关Status信息返回.
接收文件块请求
public void PostPackage(ChannelAdapter adapter, Beetle.FileTransfer.PostPackage e)
{
PostPackageResponse response = new PostPackageResponse();
response.FileID = e.FileID;
response.ID = e.ID;
try
{
Logics.FileListItem item = fileListBox1.GetAtFileID(e.FileID);
if (item != null)
{
FileTransferUtils.FileWrite(
item.Item.FileName + ".up", e.Index, item.Item.PackageSize, e.Data);
item.Completed(e.Index);
response.Index = e.Index;
if (item.Status == Logics.FileItemStatus.Completed)
FileTransferUtils.Rename(item.Item.FileName);
}
else
{
response.Status = "不存在上传信息!";
}
}
catch (Exception e_)
{
response.Status = e_.Message;
}
adapter.Send(response);
}
接收块请求后处理也很简单,根据FileID获取相关信息,然后把数据写入到文件对应的位置中;当所有块都已经完成后把临时文件名改会来就行了。如果处理异常很简单通过设置到Status成员中告诉请求方。
以下就是请求端的代码了,其代码比接收端更加简单了
public void PostResponse(ChannelAdapter adapter, Beetle.FileTransfer.PostResponse e)
{
mResponse = e;
mResetEvent.Set();
}
public void PostPackageResponse(ChannelAdapter adapter, Beetle.FileTransfer.PostPackageResponse e)
{
Logics.FileListItem item = fileListBox1.GetAtFileID(e.FileID);
if (item != null)
{
if (string.IsNullOrEmpty(e.Status))
{
item.Completed(e.Index);
PostPacakge(item);
}
else
item.Status = Logics.FileItemStatus.Default;
}
}
private void PostPacakge(Logics.FileListItem item)
{
if (mChannel != null && mChannel.Socket != null && item.Status == Logics.FileItemStatus.Working
&& item.Item.Completed != item.Item.Packages)
{
PostPackage post = new PostPackage();
post.FileID = item.Item.FileID;
post.Index = item.Item.Completed;
post.Data = FileTransferUtils.FileRead(item.Item.FileName,
item.Item.Completed, item.Item.PackageSize);
mAdapter.Send(post);
}
}
请求端要做的工作就是发送文件传输请求,等回应后就处理PostPacakge进行文件块发送,接收到当前文件块处理成功后就发送下一块直接完成。
到这里断点续传的功能代码就已经完成,两边的程序已经可以工作。不过对于一些使用者来说希望程序更友好的表现工作情况,这个时候还得对UI下一点功夫,如看到当前传输的状态和每个文件进度情况等。

以上效果看起来很不错,那接下来就把它实现吧,程序使用ListBox来显示传输文件信息,要达到以上效果需要简单地重写一下OnDrawItem达到我们需要的。在讲述代码之前介绍一个图标网站http://www.iconfinder.com/,毕竟好的图标可以让程序生色不少。下面看下这个重写的代码:
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.OnDrawItem(e);
StringFormat ListSF;
Point imgpoint = new Point(e.Bounds.X + 2, e.Bounds.Y + 1);
ListSF = StringFormat.GenericDefault;
ListSF.LineAlignment = StringAlignment.Center;
ListSF.FormatFlags = StringFormatFlags.LineLimit | StringFormatFlags.NoWrap;
ListSF.Trimming = StringTrimming.EllipsisCharacter;
Rectangle labelrect = new Rectangle(e.Bounds.X + 44, e.Bounds.Y, e.Bounds.Width - 44, e.Bounds.Height);
if (Site == null || Site.DesignMode == false)
{
if (e.Index >= 0)
{
FileListItem item = (FileListItem)Items[e.Index];
LinearGradientBrush brush;
brush =
new LinearGradientBrush(e.Bounds, Color.FromArgb(208, 231, 253),
Color.FromArgb(10, 94, 177), LinearGradientMode.Horizontal);
double pent = (double)item.Item.Completed / (double)item.Item.Packages;
using (brush)
{
e.Graphics.FillRectangle(brush, e.Bounds.X + 40, e.Bounds.Y + 2, Convert.ToInt32((e.Bounds.Width - 40) * pent), e.Bounds.Height - 4);
}
if (item.Status == FileItemStatus.Working)
{
mImgList.Draw(e.Graphics, imgpoint, 1);
}
else if (item.Status == FileItemStatus.Completed)
{
mImgList.Draw(e.Graphics, imgpoint, 2);
}
else
{
mImgList.Draw(e.Graphics, imgpoint, 0);
}
e.Graphics.DrawString(item.ToString(),new Font("Ariel", 9), new SolidBrush(Color.Black),labelrect, ListSF);
}
}
}
重绘代码就是根据当前文件的进度内容来计算出填冲的宽度,还有根据当前文件状态绘制不同的图标,是不是比较简单:)
整个功能完成了看下总体的效果怎样:

下载完整代码
如果需要Smark名称空间的代码可以到 http://smark.codeplex.com/