[转]简单实现tcp/ip下的文件断点续传

其实在tcp/ip协议中传输文件可以保证传输的有效性,但有一个问题文件传了一部分连接意外断开了怎样;那这种情况只能在重新连接后继续传输,由 于文件那部分已经传了那部分没有完成并不是tcp/ip的范围,所以需要自己来制定协议达到到这个目的。实现这个续传的协议制定其实也是非常简单,通过协 议把文件按块来划分,每完成一个块就打上一个标记;即使是连接断了通过标记状态就知道还需要传那些内容。下面通过beetle来实现一个简单断点续传的程 序(包括服务端和客户端)。

      在实现之前先整理一下流程思路,首先提交一个发送请求信息包括(文件名,块大小,块的数量等),等对方确认后就进行文件块发送,对方接收块写入后返回一个标记,然后再继续发直到所有发送完成。思路明确后就制定协了:

文件传输申请信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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属性,可以用来提供一个错误的描述,如果无有任何值的情况说明对方允许这个行为.

文件块传输信息

1
2
3
4
5
6
7
8
9
10
public class PostPackage:MessageBase
{
    public byte[] Data;
    public int Index;
}
public class PostPackageResponse : MessageBase
{
    public int Index;
    public string Status;
}

文件块传输也是一个请求,一个应答;分别带的信息就是块数据信息和块的位置,同样也是根据Status信息来标记块的处理是否成功。

      结构定义完成了,那就进行逻辑处理部分;不过为了调用更方便还需要封装一些东西,如根据块大小来划分文件块的数目,获取某一文件块的内容和写入文件某一些的内容等功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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如果创建一个服务和绑定分包机制,在这里就不多说了;看下接收的逻辑是怎样处理了.

接收传文件请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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信息返回.

接收文件块请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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成员中告诉请求方。

以下就是请求端的代码了,其代码比接收端更加简单了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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/,毕竟好的图标可以让程序生色不少。下面看下这个重写的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 @ 2015-05-30 14:21  Net-Spider  阅读(489)  评论(1)    收藏  举报