zoukankan      html  css  js  c++  java
  • 【网络编程】文件传输之断点续传与多线程传输

    PS: 前段时间做一个C#版多线程断点续传的多文件上传程序,最后参考了冷风的文件分包合并方案(方案二)效果不错,在此感谢。

    木马编程DIY第13篇之文件传输 3断点续传与多线程传输

    继木马编程DIY的上两篇,现在我们开始讨论断点续传与多线程文件传输的实现.其实这两项功能是下载软件所
    必不可少的功能了,现在我们把它加到自己的木马中来感受感受.提到多线程下载,首先向网络蚂蚁的作者
    洪以容前辈致敬,正是由于网络蚂蚁而使得多线程下载被关注并流行起来.在这本篇文章中我们将简单的实现
    支持断点续传和多线程传输的程序.为了更清晰的说明问题,我们将断点续传与多线程传输分别用两个程序来实现


    多线程传输实现

    实现原理

    将源文件按长度为分为N块文件,然后开辟N个线程,每个线程传输一块,最后合并所有线线程文件.比如
    一个文件500M我们按长度可以分5个线程传输.第一线程从0-100M,第二线程从100M-200M......最后合并5个线程文件.

    实现流程

    1.客户端向服务端请求文件信息(名称,长度)
    2.客户端跟据文件长度开辟N个线程连接服务端
    3.服务端开辟新的线程与客户端通信并传输文件
    4.客户端将每线程数据保存到一个文件
    5.合并所有线程文件

    编码实现

    大体说来就是按以上步骤进行,详细的实现和一些要点,我们跟据以上流程在编码中实现

    结构定义

    在通信过程中需要传递的信息包括文件名称,文件长度,文件偏移,操作指令等信息,为了方便操作我们定义如下结构
    typedef struct
    {
            char        Name[100];        //文件名称
            int                FileLen;        //文件长度
            int                CMD;                //操作指令
            int                seek;                //线程开始位置
            SOCKET  sockid;               
    }FILEINFO;

    1.请求文件信息

    客户端代码如下:
            FILEINFO fi;
            memset((char*)&fi,0,sizeof(fi));
            fi.CMD=1;                //得到文件信息

            if(send(client,(char*)&fi,sizeof(fi),0)==SOCKET_ERROR)
            {
                    cout<<"Send  Get FileInfo Error\n";
            }


    服务端代码如下:

            while(true)
            {
                    SOCKET client;
                    if(client=accept(server,(sockaddr *)&clientaddr,&len))
                    {
                            FILEINFO RecvFileInfo;
                            memset((char*)&RecvFileInfo,0,sizeof(RecvFileInfo));
                            if(recv(client,(char*)&RecvFileInfo,sizeof(RecvFileInfo),0)==SOCKET_ERROR)
                            {
                                    cout<<"The Clinet Socket is Closed\n";
                                    break;
                            }else
                            {
                                    EnterCriticalSection(&CS);                //进入临界区
                                    memcpy((char*)&TempFileInfo,(char*)&RecvFileInfo,sizeof(RecvFileInfo));
                                    switch(TempFileInfo.CMD)
                                            {
                                            case 1:
                                                     GetInfoProc        (client);
                                                     break;
                                            case 2:
                                                     TempFileInfo.sockid=client;
                                                     CreateThread(NULL,NULL,GetFileProc,NULL,NULL,NULL);
                                                     break;
                                            }
                                    LeaveCriticalSection(&CS);                //离开临界区
                            }
                    }
            }
    在这里服务端循环接受连接,并跟据TempFileInfo.CMD来判断客户端的请求类型,1为请求文件信息,2为下载文件
    因为在下载文件的请求中,需要开辟新的线程,并传递文件偏移和文件大小等信息,所以需要对线程同步.这里使用临界区
    其文件信息函数GetInfoProc代码如下:
    DWORD GetInfoProc(SOCKET client)
    {
            CFile        file;
            if(file.Open(FileName,CFile::modeRead|CFile::typeBinary))
            {
                    int FileLen=file.GetLength();
                    if(send(client,(char*)&FileLen,sizeof(FileLen),0)==SOCKET_ERROR)
                    {
                            cout<< "Send FileLen Error\n";
                    }else
                    {
                            cout<< "The Filelen is "<<FileLen<<"\n\n";
                    }
            }
            return 0;
    }
    这里主要是向客户端传递文件长度,而客户端收到长度后则开辟线程进行连接传输文件

    2.客户端跟据长度开辟线程

    其实现代码如下:
            FILEINFO FI;
            int FileLen=0;
            if(recv(client,(char*)&FileLen,sizeof(FileLen),0)==SOCKET_ERROR)//接受文件长度
            {
                    cout<<"Recv FileLen Error\n";
            }else
            {
                    cout<<"FileLen is "<<FileLen<<"\n";
                    int COUNT_SIZE=FileLen/5;                        //每线程传输大小                       
                    for(int i=0;i<5;i++)                                //分5个线程传输
                    {
                            EnterCriticalSection(&CS);                //进入临界区
                            memset((char*)&FI,0,sizeof(FI));
                            FI.CMD=2;                                //请求下载文件
                            FI.seek=i*COUNT_SIZE;                        //线程文件偏移
                            if(i+1==5)                                //最后一线程长度为总长度减前4个线程长度
                            {
                                    FI.FileLen=FileLen-COUNT_SIZE*i;
                            }else
                            {
                                    FI.FileLen=COUNT_SIZE;
                            }
                            Thread[i]=CreateThread(NULL,NULL,GetFileThread,&i,NULL,NULL);
                            Sleep(500);
                            LeaveCriticalSection(&CS);                //离开临界区
                    }
            }
            WaitForMultipleObjects(5,Thread,true,INFINITE);                //等所有线程结束
    这里默认开辟5个线程传输,当然可以改为想要的线程数目,仍然用临界区来实现线程的同步问题

    3.服务端开辟线程传输数据

    在1.请求文件信息中以说明了服务端的结构,这里主要介绍线程函数的实现,其代码如下:
    DWORD WINAPI GetFileProc(LPVOID lparam)
    {
            EnterCriticalSection(&CS);                        //进入临界区
            int FileLen=TempFileInfo.FileLen;
            int Seek=TempFileInfo.seek;
            SOCKET client=TempFileInfo.sockid;
            LeaveCriticalSection(&CS);                        //离开临界区

            CFile        file;
            if(file.Open(FileName,CFile::modeRead|CFile::typeBinary))
            {
                    file.Seek(Seek,CFile::begin);                //指针移至偏移位置
                    char *date=new char[FileLen];
                    int nLeft=FileLen;
                    int idx=0;
                    file.Read(date,FileLen);
                    while(nLeft>0)
                    {
                            int ret=send(client,&date[idx],nLeft,0);
                            if(ret==SOCKET_ERROR)
                            {
                                    cout<<"Send Date Error \n";
                                    break;
                            }
                            nLeft-=ret;
                            idx+=ret;
                    }
                    file.Close();
                    delete[] date;
            }else
            {
                    cout<<"open the file error\n";
            }
            closesocket(client);
            return 0;
    }
    还是比较简单的,主要是获取线程的文件长度和偏移,并移动文件指针到偏移处,最后读取发送数据,而客户端
    接受数据并写入文件.

    4.客户端将线程数据保存到文件

    GetFileThread的实现代码如下:
    DWORD WINAPI GetFileThread(LPVOID lparam)
    {
            char TempName[MAX_PATH];
            sprintf(TempName,"TempFile%d",*(DWORD*)lparam);        //每线程的文件名为"TempName"+线程数
            SOCKET client;
            SOCKADDR_IN serveraddr;
            int port=5555;
            client=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
            serveraddr.sin_family=AF_INET;
            serveraddr.sin_port=htons(port);
            serveraddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
            if(connect(client,(SOCKADDR*)&serveraddr,sizeof(serveraddr))==INVALID_SOCKET)
            {
                    cout<<"Connect Server Error\n";
            }
            EnterCriticalSection(&CS);                //进入临界区
            if(send(client,(char*)&FI,sizeof(FI),0)==SOCKET_ERROR)
            {
                    cout<<"Send  GetFile Error\n";
                    return 0;
            }
            CFile        file;
            int FileLen=FI.FileLen;                        //文件长度
            int Seek=FI.seek;                        //文件偏移
            LeaveCriticalSection(&CS);                //离开临界区

            if(file.Open(TempName,CFile::modeWrite|CFile::typeBinary|CFile::modeCreate))
            {
                    char *date = new char[FileLen];
                    int nLeft=FileLen;
                    int idx=0;
                    while(nLeft>0)
                    {
                            int ret=recv(client,&date[idx],nLeft,0);
                            if(ret==SOCKET_ERROR)
                            {
                                    cout<<"Recv Date Error";
                                    break;
                            }
                            idx+=ret;
                            nLeft-=ret;
                    }
                    file.Write(date,FileLen);
                    file.Close();
                    delete[] date;
            }else
            {
                    cout<<"Create File Error\n";
            }
            return 0;
    }
    在此线程函数中,将每线程传输的数据存为一个文件,文件名为"TempName"+线程数,只所以存成单独的文件是
    因为比较直观且容易理解,但如果文件很大的话这个方法并不好,因为合并文件又会花费很多时间,另一个方法
    是 创始一个文件,让每个线程写入文件的不同偏移
    ,这样就可以不必单独合并文件了,但要记得打开文件时
    加入CFile::shareDenyNone属性.这样整个过程就完成了.最后一步合并线程文件

    5.合并线程文件
    int UniteFile()                //合并线程文件
    {
            cout<<"Now is Unite Fileing...\n";
            int                len;
            char        *date;
            CFile        file;
            CFile        file0;
            /*其它文件......*/

            if(file.Open(FileName,CFile::modeCreate|CFile::typeBinary|CFile::modeWrite))//创建文件
            {
                    file0.Open("TempFile0",CFile::modeRead|CFile::typeBinary);//合并第一线程文件
                    len=file0.GetLength();
                    date=new char[len];
                    file0.Read(date,len);
                    file.SeekToEnd();
                    file.Write(date,len);
                    file1.Open("TempFile1",CFile::modeRead|CFile::typeBinary);//合并第二线程文件
                    len=file1.GetLength();
                    date=new char[len];
                    file1.Read(date,len);
                    file.SeekToEnd();
                    file.Write(date,len);
                    /*合并其它线程......*/

                   
                    file0.Close();
                    file1.Close();
                    /*.......*/
                    delete[] date;
                   
                    return true;
            }else
            {
                    return false;
            }

    }
    这个简单,就是打开一个文件读取到缓冲区,写入文件,再打开第二个......现在多线程传输部分就介绍完了
    下面讨论断断点续传的实现


    断点续传

    所谓的断点续传就是指:文件在传输过程式中被中断后,在重新传输时,可以从上次的断点处开始传输,这样就可
    节省时间,和其它资源.

    实现关键

    在这里有两个关键点,其一是检测本地已经下载的文件长度和断点值,其二是在服务端调整文件指针到断点处

    实现方法

    我们用一个简单的方法来实现断点续传的功能.在传输文件的时候创建一个临时文件用来存放文件的断点位置
    在每次发送接受文件时,先检查有没有临时文件,如果有的话就从临时文件中读取断点值,并把文件指针移动到
    断点位置开始传输,这样便可以做到断点续传了

    实现流程

    首次传输其流程如下

    1.服务端向客户端传递文件名称和文件长度
    2.跟据文件长度计算文件块数(文件分块传输请参照第二篇文章)
    3.客户端将传输的块数写入临时文件(做为断点值)
    4.若文件传输成功则删除临时文件

    首次传输失败后将按以下流程进行

    1.客户端从临时文件读取断点值并发送给服务端
    2.服务端与客户端将文件指针移至断点处
    3.从断点处传输文件

    编码实现

    因为程序代码并不复杂,且注释也比较详细,这里就给出完整的实现

    其服务端实现代码如下:
    int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
    {
            cout<<"\t\t服务端-断点续传"<<"\t  作者:冷风\n\n"<<"请输入被下载的文件路径 如 C:\\File.rar \n\n"<<"文件路径:  ";
            cin        >>FilePath;
            /*这部分为网络参数与设置,详细请参照源代码......*/
            while(true)
            {
                    if(client=accept(server,(sockaddr *)&clientaddr,&len))
                    {
                            cout<<"have one connect\n";
                            int nCurrentPos=0;//接受断点值
                            if(recv(client,(char*)&nCurrentPos,sizeof(nCurrentPos),0)==SOCKET_ERROR)
                            {
                                    cout<<"The Clinet Socket is Closed\n";
                                    break;
                            }else
                            {
                                    cout<<"The Currentpos is The"<<nCurrentPos<<"\n";
                                    GetFileProc        (nCurrentPos,client);
                            }
                    }
            }
            closesocket(server);
            closesocket(client);
            WSACleanup();
            return 0;
            return 0;
    }

    DWORD  GetFileProc        (int nCurrentPos,SOCKET client)
    {
            cout <<"Get File Proc is ok\n";
            CFile        file;
            int                nChunkCount=0;        //文件块数
            if(file.Open(FilePath,CFile::modeRead|CFile::typeBinary))
            {
                    if(nCurrentPos!=0)
                    {
                            file.Seek(nCurrentPos*CHUNK_SIZE,CFile::begin);        //文件指针移至断点处
                            cout<<"file seek is "<<nCurrentPos*CHUNK_SIZE<<"\n";
                    }
                    int FileLen=file.GetLength();
                    nChunkCount=FileLen/CHUNK_SIZE;                                //文件块数
                    if(FileLen%nChunkCount!=0)
                            nChunkCount++;
                    send(client,(char*)&FileLen,sizeof(FileLen),0);                //发送文件长度
                    char *date=new char[CHUNK_SIZE];
                    for(int i=nCurrentPos;i<nChunkCount;i++)                //从断点处分块发送
                    {       
                            cout<<"send the count"<<i<<"\n";
                            int nLeft;
                            if(i+1==nChunkCount)                                //最后一块
                                    nLeft=FileLen-CHUNK_SIZE*(nChunkCount-1);
                            else
                                    nLeft=CHUNK_SIZE;
                            int idx=0;
                            file.Read(date,CHUNK_SIZE);
                            while(nLeft>0)
                            {
                                    int ret=send(client,&date[idx],nLeft,0);
                                    if(ret==SOCKET_ERROR)
                                    {
                                            cout<<"Send The Date Error \n";
                                            break;
                                    }
                                    nLeft-=ret;
                                    idx+=ret;
                            }
                    }
                    file.Close();
                    delete[] date;
            }else
            {
                    cout<<"open the file error\n";
            }
            return 0;
    }

    客户端实现代码如下:
    int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
    {
            cout<<"\t\t客户端-断点续传"<<"\t  作者:冷风\n\n"<<"请输入保存文件的路径 如 C:\\Save.RAR \n\n"<<"文件路径:  ";
            cin        >>FilePath;
            /*网络参数初示化,详细请参照源代码......*/
            if(connect(client,(SOCKADDR*)&serveraddr,sizeof(serveraddr))==INVALID_SOCKET)
            {
                    cout<<"Connect Server Error";
                    return 0;
            }
            int FileLen=0;

            int nCurrentPos=0; //断点位置

            UINT OpenFlags;       

            CFile PosFile;
            if(PosFile.Open("PosFile.temp",CFile::modeRead|CFile::typeBinary))//如果有临时文件则读取断点
            {
                    PosFile.Read((char*)&nCurrentPos,sizeof(nCurrentPos));        //读取断点位置
                    cout<<"The File Pos is "<<nCurrentPos<<"\n";
                    nCurrentPos=nCurrentPos+1;                                //从断点的下一块开始
                    PosFile.Close();
                    send(client,(char*)&nCurrentPos,sizeof(nCurrentPos),0);        //发送断点值
                    OpenFlags=CFile::modeWrite|CFile::typeBinary;                //文件为可写
            }
            else
            {
                    send(client,(char*)&nCurrentPos,sizeof(nCurrentPos),0);        //无断点文件nCurrentPos为0
                    OpenFlags=CFile::modeWrite|CFile::typeBinary|CFile::modeCreate;//创建文件方式
            }

            if(recv(client,(char*)&FileLen,sizeof(FileLen),0)!=0)//接受文件长度
            {
                    int                nChunkCount;
                    CFile        file;
                    nChunkCount=FileLen/CHUNK_SIZE;                //计算文件块数
                    if(FileLen%nChunkCount!=0)
                    {
                            nChunkCount++;
                    }

                    if(file.Open(FilePath,OpenFlags))
                    {
                            file.Seek(nCurrentPos*CHUNK_SIZE,CFile::begin);        //文件指针移至断点处

                            char *date = new char[CHUNK_SIZE];

                            for(int i=nCurrentPos;i<nChunkCount;i++)        //从断点处开始写入文件
                            {
                                    cout<<"Recv The Chunk is "<<i<<"\n";
                                    int nLeft;
                                    if(i+1==nChunkCount)                                                //最后一块
                                            nLeft=FileLen-CHUNK_SIZE*(nChunkCount-1);
                                    else
                                            nLeft=CHUNK_SIZE;
                                    int idx=0;
                                    while(nLeft>0)
                                    {
                                            int ret=recv(client,&date[idx],nLeft,0);
                                            if(ret==SOCKET_ERROR)
                                            {
                                                    cout<<"Recv Date Error";
                                                    return 0;
                                            }
                                            idx+=ret;
                                            nLeft-=ret;
                                    }
                                    file.Write(date,CHUNK_SIZE);
                                    CFile        PosFile;                //将断点写入PosFile.temp文件
                                    int seekpos=i+1;
                                    if(PosFile.Open("PosFile.temp",CFile::modeWrite|CFile::typeBinary|CFile::modeCreate));
                                    {
                                            PosFile.Write((char*)&seekpos,sizeof(seekpos));
                                            PosFile.Close();
                                    }
                            }
                            file.Close();
                            delete[] date;
                    }       
                    if(DeleteFile("PosFile.temp")!=0)
                    {
                            cout<<"文件传输完成";
                    }
            }
            return 0;
    }
    客户端运行时会试图打开临时文件,如果存在则读取断点值,如果不存在则断点为0,打开文件后将文件指针移至
    断点处开始接受数据,每接受一块就把当前块的数值存入临时文件.其实现代码比较简单结合上面的流程介绍
    看代码应该没什么难度,所以我也就不画蛇添足了。

    到此文件传输部分就介绍完毕了,在写文件传输这一系列程序的过程中
    界面实现主要参考了VC知识库王景生的<<VC控件TreeCtrl与ListCtrl演示>>一文
    在功能实现一篇中主要参考了"草草"的SEU_PEEPER木马的源代码(强烈推荐一下,草草如果看到了一定要请客哦)
    而在本篇中主要参考了 2005电脑报 王育文<<简单文件传输实现>>一文,如果有问题的话参考一下上面的著作
    相信会有很大收获的,当然更欢迎到黑或我的BLOG(Http:// blog.csdn.net/chinafe)上讨论

  • 相关阅读:
    简谈单线程多线程的理解
    简谈vuex的理解
    简谈自己对redux的理解
    react-native-1
    react中信息传递的几种方式
    简谈对虚拟Dom的理解
    React-小总结
    移动端解决边框1像素问题
    JavaScript标记上的defer与async的作用与区别
    jQuery.smoove — jQuery和CSS3炫酷滚动页面内容元素动画特效插件
  • 原文地址:https://www.cnblogs.com/ronli/p/1724257.html
Copyright © 2011-2022 走看看