简单实现tcp/ip下的文件断点续传
其实在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/