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)上讨论

  • 相关阅读:
    Java实现 LeetCode 343 整数拆分(动态规划入门经典)
    Java实现 LeetCode 342 4的幂
    Java实现 LeetCode 342 4的幂
    Java实现 LeetCode 342 4的幂
    Java实现 LeetCode 341 扁平化嵌套列表迭代器
    Java实现 LeetCode 341 扁平化嵌套列表迭代器
    Java实现 LeetCode 341 扁平化嵌套列表迭代器
    Java实现 LeetCode 338 比特位计数
    H264(NAL简介与I帧判断)
    分享一段H264视频和AAC音频的RTP封包代码
  • 原文地址:https://www.cnblogs.com/ronli/p/1724257.html
Copyright © 2011-2022 走看看