简单实现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);
                }
            }
        }

重绘代码就是根据当前文件的进度内容来计算出填冲的宽度,还有根据当前文件状态绘制不同的图标,是不是比较简单:)

整个功能完成了看下总体的效果怎样:

下载完整代码

FileTransfer.rar (649.79 kb) 

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

 

 

posted @ 2012-02-24 10:29  beetlex  阅读(11167)  评论(4编辑  收藏  举报