http协议多线程下载,支持断点续传(后续再完善)

9 篇文章 0 订阅
7 篇文章 0 订阅

#include <iostream>
#include <iosfwd>
#include <fstream>
#include <regex>
#include <string>
#include <winsock2.h>
#include <pthread.h>
#include <stdio.h>
#include <map>
#include <queue>

/**
*要实现多线程http协议下载文件的功能,支持断点续传和多线程同时下载
*http请求使用Range: bytes完成
*同时写n个临时文件,最后执行完成拼接在一起
*目前支持多线程下载,自动拼接文件,
*后续增加一个守护进程,判断是否都下载完成,记录下载进度,若是未下载完成,继续下载,若是下载完成,就完成文件拼接
**/

using namespace std;

SOCKET sock[100];
//测试主机和端口
const char *testHostName="www.qxmuye.com";
const short testPort=80;
const string testPortChar="80";
//文件地址
char *url="/article/UploadPic/2015-9/2015982144994925.gif";
const string dataFile="D://testmap//test.gif";
//临时文件地址
const string tmpFile="D://testmap//tmpfile";
//记录文件,记录下载进度,和下载成功标志位
const string properFile="D://testmap//download.properties";
//临时记录文件
const string properTmpFile="D://testmap//download_tmp.properties";
//配置文件
fstream fileProper;
/****
*   格式
*第一行:0|1 当前执行的下载目标  (第一位标志位,0为未下载完成,1为下载完成,空格,当前下载目标)
*第二行:0|1 已下载大小 (第一位标志位,0为未下载完成,1为下载完成,空格,已下载数量)
*  .
*  .
*  .
* 分块
*下载完成,直接将第一行置为1,清空其他数据。
*
*****/
//块下载进度记录读取数组
 int downSize[100];
//块下载进度记录写入数组
 int downEndSize[100];
//块下载最大容量组
int maxSize[100];
//块下载进度读取标志组
 bool downFlag[100];
//块下载进度写入标志组
 bool downEndFlag[100];
//断点续传下载块数量
 int downNum=0;
//记录日志是否下载成功标志位
 char *downFinishFlag;
//记录文件中下载目标地址或已下载数量
 char *downFileUrl;
//记录日志文件读取缓冲区
 char fileTmpbuf[512];
//是否结束下载标志位
 bool isEnded=false;
//是否完成下载标志位
 bool isFinished=false;
//分块下载的块大小
const int getSize=100000;
//记录下载进度的间隔时间
const int sleepTime=1000;
//等待线程是否结束
 bool isWaitFinished=false;

int fileNameInt;

//发送http请求包
bool sendHttpQuery(string sendQueryStr,int sockNum){
    int n=0;
   //初始化socket
    sock[sockNum] = socket(AF_INET, SOCK_STREAM, 0);

    if (sock[sockNum] == INVALID_SOCKET)
    {
        cout << "建立socket失败! 错误码: " << WSAGetLastError() << endl;
        return false;
    }
    sockaddr_in sa = { AF_INET };
    u_short port=0;
    sa.sin_port = htons(port);
    sa.sin_addr.s_addr = htonl(INADDR_ANY);
    n = bind(sock[sockNum], (sockaddr*)&sa, sizeof(sa));
    if (n == SOCKET_ERROR)
    {
        cout << "bind函数失败! 错误码: " << WSAGetLastError() << endl;
        return false;
    }
    struct hostent *p = gethostbyname(testHostName);
    if (p == NULL)
    {
        cout << "主机无法解析出ip! 错误吗: " << WSAGetLastError() << endl;
        return false;
    }

    sa.sin_port = htons(testPort);//随机分配
    memcpy(&sa.sin_addr, p->h_addr, 4);
    //sa.sin_addr.S_un.S_addr = inet_addr(*(p->h_addr_list));
    //cout << *(p->h_addr_list) << endl;
    //连接
    n = connect(sock[sockNum], (sockaddr*)&sa, sizeof(sa));
    if (n == SOCKET_ERROR)
    {
        cout << "connect函数失败! 错误码: " << WSAGetLastError() << endl;
        return false;
    }

    //按照http送GET请求
    cout<<"发送http请求:"+sendQueryStr<<endl;
    if (SOCKET_ERROR == send(sock[sockNum], sendQueryStr.c_str(), sendQueryStr.size(), 0))
    {
        cout << "send error! 错误码: " << WSAGetLastError() << endl;
        closesocket(sock[sockNum]);
        return false;
    }
    return true;
}

//文件拼接,将第二个参数文件中的内容复制给第一个参数中的文件,两个文件要求已打开
void getGolobalFile(ofstream &fGolobal,ifstream &fPart){
    //fGolobal.open(dataFile,ios::app|ios::binary);
    char tmpFileChar[1001];
    if(!fPart.is_open()){
        cout<<"临时文件未打开!"<<endl;
    }
    if(!fGolobal.is_open()){
        cout<<"总文件未打开!"<<endl;
    }
    while(!fPart.eof()){
        fPart.read(tmpFileChar,1000);
        fGolobal.write(tmpFileChar,fPart.gcount());
        //char readNumChar[10];
        //sprintf(readNumChar,"%d",fPart.gcount());
        //cout<<"复制字符长度:"+(string)readNumChar<<endl;
        //memset(tmpFileChar,0,1024);
    }
    cout<<"文件复制完成!"<<endl;
}

//从http的返回头信息中解析出文件长度
char * getResponseLeng(const char receiveBuf[1024]){

    //找到Content-Range: bytes 0-1/之后的字符串lengStartChar
    char *lengStartChar = strstr(receiveBuf, "Content-Range: bytes 0-1/");
    int tmpSize=sizeof("Content-Range: bytes 0-1/");//注意sizeof长度将结束符计算进去,后面需要减掉1
    //char *tmpSizeChar;
    //sprintf(tmpSizeChar,"%d",tmpSize);
    //cout<<(string)tmpSizeChar<<endl;
    //char *lengEndchar=strstr(buf, "\r\n");
    int i=0;
    bool isEnd=false;
    //在lengStartChar找到\r\n之前的位置
    while(!isEnd){
        if(lengStartChar[i+1]!='\r')i++;
        else{
            if(lengStartChar[i+2]=='\n')isEnd=true;
            else i++;
        }
    }

    //将lengStartChar中关于长度的一段赋值给长度字符串,注意减去结束符
    char *leng=new char(10);
    memset(leng,0x00,10);
    memcpy(leng,lengStartChar+tmpSize-1,i-tmpSize+2);
    //cout<<"111  "+(string)leng+"  111"<<endl;

    return leng;
}

//下载线程,将特定的文件块下载到特定的临时文件中,根据参数设置特定的临时文件
void *downloadTmpFile(void *args){
    int n;
    //修改子线程为分离状态,不阻塞主线程
    pthread_detach(pthread_self());
    int tmpNum=*(int *)args;//先将指针转为int型指针,再取值
    char tmpBuf[1024];
    memset(tmpBuf, 0, sizeof(tmpBuf));

    char tmpNumChar[10];
    sprintf(tmpNumChar,"%d",tmpNum);

    if(downEndSize[tmpNum]<maxSize[tmpNum]){

        ofstream fileTmp;
        //临时文件新建或打开
        fileTmp.open(tmpFile+(string)tmpNumChar ,ios::app|ios::out|ios::binary);

        cout << " 文档打开 "+tmpFile+(string)tmpNumChar  << endl;
        if(!fileTmp.is_open()){
           cout << " 文档打开失败! "+tmpFile+(string)tmpNumChar << endl;
        }

        char *startMark=new char(10);
        char *endMark=new char(10);//设置开始地址,结束地址,注意
        memset(startMark,0,10);
        memset(endMark,0,10);
        sprintf(startMark,"%d",(tmpNum*getSize+downSize[tmpNum]));
        sprintf(endMark,"%d",(tmpNum*getSize+(getSize-1)));
        cout<<downSize[tmpNum]<<"  "<<getSize-1<<"  "<<endl;
        string strFirst="GET "+(string)url+" HTTP/1.1\r\nHost:"+(string)testHostName+":"+testPortChar+"\r\nRange:bytes="+(string)startMark+"-"+(string)endMark+"\r\nConnection:Close\r\n\r\n";
        //发送http请求
        if(!sendHttpQuery(strFirst,tmpNum)){
            cout<<" http 请求发送失败! "<<endl;
        }
        n = recv(sock[tmpNum], tmpBuf, sizeof(tmpBuf)-1, 0);
        char *cpos = strstr(tmpBuf, "\r\n\r\n");
        //移动到断点位置继续下载
        fileTmp.seekp(downSize[tmpNum],ios::beg);
        fileTmp.write(cpos + strlen("\r\n\r\n"), n - (cpos - tmpBuf) - strlen("\r\n\r\n"));
        downEndSize[tmpNum]=downEndSize[tmpNum]+(n - (cpos - tmpBuf) - strlen("\r\n\r\n"));
        while ((n = recv(sock[tmpNum], tmpBuf, sizeof(tmpBuf)-1, 0)) > 0)
        {
            //写入临时文件
            fileTmp.write(tmpBuf, n);
            //记录写入文件的缓冲区
            downEndSize[tmpNum]=downEndSize[tmpNum]+n;
            //cout<<downEndSize[tmpNum]<<"  "<<tmpNum<<"  "<<n<<endl;
        }
        fileTmp.close();
    }
}

//根据参数,替换记录文件中第i行为字符串strTmp
/**
bool changeLine(int deleteLine,char *strTmp){
    fileProper.close();

    char strLine[1024];
    memset(strLine,0x00,1024);
    fileProper.open(properFile,ios::in|ios::out);
    //fstream fileTmpStream ;
    //fileTmpStream.open(properTmpFile,ios::in|ios::out);
    while(!fileProper.eof()){
        if(i==deleteLine){
            fileTmpStream<<strTmp<<endl;
        }else{
            fileProper.getline(strLine,1023);
            fileTmpStream<<strLine<<endl;
        }
    }
    fileProper.close();
    remove(properFile);
}
**/

//写记录文件
//参数 0 未结束,1 已结束 2 被终止
void writeDownFile(int endMode){
    //清空原有内容
    fileProper.close();
    FILE *fp=fopen(properFile.c_str(),"w");
    fclose(fp);
    fileProper.open(properFile,ios::in|ios::out);
    if(endMode==1){
        //如果已结束,那么只在记录文件中写结束标志和目的url,不写具体块下载信息
        fileProper<<"1|"+(string)url<<endl;
        int i=0;
        while(i<=downNum){
            //清空块下载信息
            fileProper<<""<<endl;
            i++;
        }
    }else{
        //未结束和被终止,第一行写未结束标志和目的url,后面每行写块是否结束和块的下载进度信息
        fileProper<<"0 "+(string)url<<endl;
        int i=0;
        while(i<=downNum){
            if(downEndSize[i]<maxSize[i]){
                //如果未完成,记录标志和已下载字节
                char charTmp[10],charTmp2[10];
                memset(charTmp,0,10);
                memset(charTmp2,0,10);
                sprintf(charTmp,"%d",downEndSize[i]);
                sprintf(charTmp2,"%d",maxSize[i]);
                fileProper<<"0|"+(string)charTmp+"|"+(string)charTmp2<<endl;
            }else{
                //如果已完成,只记录标志
                char charTmp2[10];
                memset(charTmp2,0,10);
                sprintf(charTmp2,"%d",maxSize[i]);
                fileProper<<"1|"+(string)charTmp2+"|"+(string)charTmp2<<endl;
            }
            i++;
        }
    }
}

//等待下载完成,每隔一定时间记录下载进度
void *recordDownLog(void *args){
    //修改子线程为分离状态,不阻塞主线程
    //pthread_detach(pthread_self());
    while(!isFinished&&!isWaitFinished){
        //如果所有已下载数量都已达到最大,那么下载完成
        isFinished=true;
        int i=0;
        //char *inputChar;
        //每秒判断一次是否读写完毕
        Sleep(sleepTime);
        //判断所有块的数据是否都已下载完
        while(i<=downNum){
            if(downEndSize[i]<maxSize[i]){
                isFinished=false;
            }
            i++;
        }
        //只要有一个满足,说明下载完成或者被中断
        if(isFinished||isEnded){
            if(isFinished){
                //下载完成
                writeDownFile(1);
            }else{
                //下载被终止
                writeDownFile(2);
            }
        }else{
            //未结束或被终止,写记录文件记录下载信息
            writeDownFile(0);
        }
    }
    cout<<"结束!"<<endl;
    isWaitFinished=true;
}


int main()
{
    char buf[1024];
    memset(buf, 0, sizeof(buf));
    memset(fileTmpbuf, 0, sizeof(fileTmpbuf));
    //memset(downFileUrl, 0, 500);
    //memset(downFileUrl,0,sizeof(downFinishFlag));

    WORD version(0);
    WSADATA wsadata;
    int socket_return(0);
    version = MAKEWORD(2,0);
    socket_return = WSAStartup(version,&wsadata);
    if (socket_return != 0)
    {
        return 0;
    }

    //标志位,判断继续下载(true)还是重新下载(false)
    bool isContinueDown=false;
    //一行最多500个,超过出错,
    //char fileLogLine[501];
    memset(fileTmpbuf,0x00,sizeof(fileTmpbuf));
    fileProper.open(properFile,ios::in|ios::out);

    /**
    *查找记录文件,根据文件不同情况进行不同操作
    *记录文件不存在,记录文件为空或记录文件第一个字符为1,都重新开始下载
    *记录文件第一个字符为0,继续下载原有文件
    **/
    //判断文件是否存在
    if(!fileProper){
        //文件不存在
        isContinueDown=false;
        fileProper.open(properFile,ios::out);
        fileProper.close();
        fileProper.open(properFile,ios::in|ios::out);
    }else{
        if(fileProper.getline(fileTmpbuf,255)){
            //拷贝第一位作为标志位
            //memcpy(downFinishFlag,fileTmpbuf,1);
            downFinishFlag=strtok(fileTmpbuf,"|");
            //downFinishFlag[1]='\0';
            //读到空,说明记录文件为空,需要重新下载
            if(fileProper.gcount()==0){
                isContinueDown=false;
            }else{
                //第一标志位为0,继续下载中断的任务
                if(downFinishFlag[0]=='0'){
                    isContinueDown=true;
                }else{
                    //第一标志位为1,说明上一个文件已经下载完成,重新下载
                    //或者其他标志位值,不合法,重新下载
                    //去掉前两位后拷贝到url缓存区
                    //memcpy(downFileUrl,fileTmpbuf+2,254);
                    downFileUrl=strtok(NULL,"|");
                    //downFileUrl[255]='\0';
                    memset(fileTmpbuf,0,sizeof(fileTmpbuf));
                    isContinueDown=false;
                }
            }
        }
    }

    /**
    *如果是继续下载断点文件,那么取记录文件中的url值作为目标
    *并取得下载块数量和每个下载块的下载情况
    **/
    if(isContinueDown==true){
        strcpy(url,downFileUrl);
        char *maxCharNum;
        while(fileProper.getline(fileTmpbuf,500)){
            //从记录文件中读取上次下载进度信息
            downFinishFlag=strtok(fileTmpbuf,"|");
            //memcpy(downFinishFlag,fileTmpbuf,1);
            //downFinishFlag[1]='\0';
            downFileUrl=strtok(NULL,"|");
            //memcpy(downFileUrl,fileTmpbuf+2,10);
            //memset(maxCharNum,0,10);
            maxCharNum=strtok(NULL,"|");
            //memcpy(maxCharNum,fileTmpbuf+13,10);
            //downFileUrl[255]='\0';
            //取得已下载数量赋值给开始下载数量组和结束下载数量组
            //cout<<"111111"<<endl;
            downSize[downNum]=atoi(downFileUrl);
            downEndSize[downNum]=atoi(downFileUrl);
            maxSize[downNum]=atoi(maxCharNum);
            downNum++;
            memset(fileTmpbuf,0,sizeof(fileTmpbuf));
        }
        //getTimeInt=downNum;
    }else{
        //如果是重新下载,将所有已下载数量置为0,所有结束下载数量组也置为0
        //将所有下载完成标志置为1,注意下载时区分是本块下载完其他未下载完还是所有都下载完成
        int i=0;
        while(i<100){
            downSize[i]=0;
            downEndSize[i]=0;
            //downFinishFlag[i]='1';
            i++;
        }
        //如果重新下载,需要取目标长度,分出块的数量
        string strFirst="GET "+(string)url+" HTTP/1.1\r\nHost:"+(string)testHostName+":"+testPortChar+"\r\nRange:bytes=0-1\r\nConnection:Close\r\n\r\n";
        //发送http请求
        if(!sendHttpQuery(strFirst,0)){
            cout<<" http 请求发送失败! "<<endl;
            return 0;
        }
        recv(sock[0], buf, sizeof(buf)-1, 0);

        cout<<buf<<endl;
        char *lengChar=getResponseLeng(buf);
        cout<<"长度为 :"+(string)lengChar<<endl;
        int lengInt=atoi(lengChar);
        //得到每次取1000长度总共需要循环的次数
        downNum=lengInt/getSize;

        //设置每个块的最大下载容量
        i=0;
        while(i<downNum){
            maxSize[i]=getSize-1;
            i++;
        }
        maxSize[downNum]=lengInt%getSize;

        //写入记录文件
        fileProper<<"0 "+(string)url<<endl;
    }

    //思路:将Content-Range: bytes 0-1/后面的值作为长度解析出来
    //再根据文件长度决定循环几次请求
    int i=0;
    int ret[downNum+1];
    pthread_t testthreadDownload[downNum+1];

    //根据记录已下载数量判断是否下载完成
    pthread_t writeFileThread;
    int writeThread=pthread_create(&writeFileThread,NULL,&recordDownLog,NULL);
    if(writeThread){
            cout<<"创建记录线程失败!"<<endl;
            return 1;
    }

    while(i<=downNum){
        //注意,使用sleep让线程得以创建,不然可能还没创建线程就已经销毁

        //判断,所有块下载线程都启动,进入线程再判断是否本块是否已完成
        //if(downFinishFlag[i]=='0'){
        ret[i]= pthread_create(&testthreadDownload[i],NULL,&downloadTmpFile,&i);
        //}
        Sleep(1000);
        //线程互斥锁打开
        //pthread_mutex_unlock(&mutex);
        if(ret[i]){
            cout<<"创建临时文件线程失败!"<<endl;
            return 1;
        }
        i++;
    }
    i=0;
    while(i<=downNum){
       pthread_join(testthreadDownload[i],NULL);
       i++;
    }

    pthread_join(writeFileThread,NULL);
    fileProper.close();

    //合并下载文件,做个判断,只有下载完成才需要合并
    if(isWaitFinished==true){
        i=0;
        //pthread_t testthreadFileAdd[getTimeInt+1];
        ofstream fileGolobal;
        ifstream fileTmp[downNum+1];
        while(i<=downNum){

            fileGolobal.open(dataFile,ios::app|ios::out|ios::binary);
            char tmpNumChar[5];
            sprintf(tmpNumChar,"%d",i);
            cout<<"打开文件"+tmpFile+(string)tmpNumChar<<endl;
            //if(fileTmp[i].is_open()){
            //    fileTmp[i].close();
            //}
            fileTmp[i].open(tmpFile+(string)tmpNumChar,ios::in|ios::binary);
            cout<<"文件打开成功!"+tmpFile+(string)tmpNumChar<<endl;
            getGolobalFile(fileGolobal,fileTmp[i]);
            fileGolobal.close();
            fileTmp[i].close();
            i++;
        }
    }
    //file.close();
    return 0;
}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值