HTTP协议进行多线程以及断点下载(没有实用意义的程序)

写完个人感觉,没有啥实用意义是因为想从服务器上下载的东西基本都是支持HTTPS协议下载的,并非HTTP了

实现功能

  1. 多线程下载
  2. 断点下载
  3. 进度条显示

具备知识点

  1. HTTP请求以及响应头
  2. 多线程下载

实现思路

  1. 根据下载地址(仅仅限于HTTP下载)来解析下载地址中的FQDN以及指定的端口号,通过函数gethostbyname获取服务器的ip地址,通过套接字地址结构的填充连接上服务器。
  2. 此时通过发送HTTP请求头,尝试是否可以连接上服务器,发送HEAD请求方法,请求HTTP的响应头,通过响应头的content-length关键字,获取资源文件的大小。
  3. 此时检查文件是否属于断点下载,其中判断是否为断点文件下载的标准是定义了.*td文件,在下载过程未结束的过程中,都是写入.*td文件中,直到下载结束将文件名称的后缀名改成之前指定的名称格式。所以.*td文件存在则说明是断点下载,否则为一个新的下载。其中若下载过程中失败的话,则删除.*td文件
  4. 根据用户制定的线程数量进行下载,其中线程数量不能超过20,若超过二十则默认为5个线程下载。
  5. 每个线程创建与服务器的连接,发送自己所需的字节数目 HTTP请求头,最后根据请求的资源写入文件中

多线程下载原理

多线程下载的原理是这样的:通常服务器同时与多个用户连接,用户之间共享带宽。如果N个用户的优先级都相同,那么每个用户连接到该服务器上的实际带宽就是服务器带宽的N分之一。可以想象,如果用户数目较多,则每个用户只能占有可怜的一点带宽,下载将会是个漫长的过程。
如果你通过多个线程同时与服务器连接,那么你就可以榨取到较高的带宽了。例如原来有10个用户都通过单一线程与服务器相连,服务器的总带宽假设为56Kbps,则每个用户(每个线程)分到的带宽是5.6Kbps,即0.7K字节/秒。如果你同时打开两个线程与服务器连接,那么共有11个线程与服务器连接,而你获得的带宽将是56/11*2=10.2Kbps,约1.27K字节/秒,将近原来的两倍。你同时打开的线程越多,你所获取的带宽就越大(原来是这样,以后每次我都通过1K个线程连接:P)。当然,这种情况下占用的机器资源也越多。有些号称“疯狂下载”的下载工具甚至可以同时打开100个线程连接服务器。

.*td文件

.td和.td.cfg文件,这二个是迅雷的临时下载文件.和配置文件,在*.td文件里是你的下载数据,.td.cfg文件是您的这个文件的配置文件,记录的是您下载这个文件的配置,(线程,存放目录,用户名,密码等等),当您的文件下载完成了以后,会自动的将你的.td.cfg配置文件删除掉,并将*.td临时下载文件的后缀名.td去掉,变成您所要正确下载的文件!! 如果您下载文件的格式是td的,说明您的这个文件还没有下载完!!请您继续下载!!

代码

download.h

#ifndef _DOWNLOAD_H
#define _DOWNLOAD_H
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/stat.h>
#include<sys/time.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<assert.h>
#include<stdlib.h>
using namespace std;

enum HTTPCODE{OK, FORBIDDEN, NOTFOUND, UNKNOWN, PARTIAL_OK};

struct file_imformation{
    char *file_path;//文件的绝对路径
    char file_name[1000];//文件解析出来的名称
    char file_name_td[1000];//建立.*td文件,判断是否为断点下载
    long int file_length;//文件的大小字节数目
};
struct thread_package{
    pthread_t pid;//线程号
    char *url;
    char *fqdn;
    int sockfd;//sockfd
    long int start;//文件下载起始位置
    long int end;//文件下载结束位置
    char file_name[1000];//文件名称
    int read_ret;//读取字节数目
    int write_ret;//写入字节数目
};
/*客户类定义*/
class Baseclient{
private:
    int sockfd;//套接字
    int port;//端口号
    int thread_number;//开辟的线程数量
    char *address;//下载地址参数
    char *address_buf;
    char *fqdn;//FQDN解析
    char http_request[1000];//http请求头填写
    char http_respond[1000];//http响应头接收
    struct sockaddr_in server;//服务器套接字地址
    struct hostent *host;//通过解析下载地址,获取IP地址
    struct thread_package Thread_package;//线程包
    struct file_imformation myfile_information;//文件信息
    enum STATUS{HTTP=0, HTTPS, HOST_WRONG};
    STATUS status;
public:
    Baseclient(int thread_num, char *addr) : thread_number(thread_num), address(addr){
        sockfd = -1;
        port = 80;//默认端口为80
        fqdn = NULL;
        status = HTTP;
        memset(http_request, 0, 1000);
        bzero(&server,sizeof(server));
        bzero(&Thread_package,sizeof(Thread_package));
        bzero(&host,sizeof(host));
        bzero(&myfile_information,sizeof(myfile_information));
    }
    
    ~Baseclient();
    STATUS parse_address();//解析下载地址
    void parse_httphead();//解析HTTP响应头
    void thread_download();//多线程下载
    void mysocket();
private:
    static void *work(void *arg);
};
#endif

download.cpp

#include"download.h"

/*解析HTTP响应码函数*/
HTTPCODE parse_HTTPCODE(const char *http_respond)
{
    char *http;
    char *get;
    char code[4];
    int len = strlen(http_respond);
    http = new char [len];
    strcpy(http, http_respond);
    get = strstr(http," ");
    get++;
    int i=0;
    while(*get != ' ')
    {
        code[i++] = *get;
        get++;
    }
    code[3] = '\0';
    delete [] http;
    cout << "code:"<< code << endl;
    if(strcmp(code,"200")==0) return OK;
    if(strcmp(code,"206")==0) return PARTIAL_OK;
    if(strcmp(code,"403")==0) return FORBIDDEN;
    if(strcmp(code, "400")==0) return NOTFOUND;
    else return UNKNOWN;
}

/*对HTTP响应码作出相应的处理*/
void deal_with_code(HTTPCODE code)
{
    int my = code;
    switch(my)
    {
        case OK:
        {
            cout << "OK\n";
            return;
        }
        case PARTIAL_OK:
        {
            cout << "PARTIAL_OK\n";
            return;
        }
        case FORBIDDEN:
        {
            cout << "该资源无权访问!\n";
            exit(0);
        }
        case NOTFOUND:
        {
            cout << "未找到该资源,请检查下载地址是否填写正确!\n";
            exit(0);
        }
    }
}

long int get_file_size(int fd)
{
    struct stat st;
    int ret = fstat(fd, &st);
    assert(ret != -1);
    return st.st_size;
}

Baseclient :: ~Baseclient()
{
    close(sockfd);
    delete [] myfile_information.file_path;
    
}
/*解析用户输入的下载地址*/
Baseclient :: STATUS Baseclient :: parse_address()
{
    char *get;
    /*判断下载地址的状态*/
    if(strstr(address,"https") != NULL)
    {
        return HTTPS;
    }
    
    /*获取FQDN*/
    get = address + 7;
    fqdn = get;//获取FQDN的起始位置
    get = strstr(get, "/");//解析出FQDN地址
    *get++ = '\0';
    host = gethostbyname(fqdn); //通过名字获取hostIP地址
    
    /*获取文件的绝对路径*/
    int len = strlen(get)+2;
    myfile_information.file_path = new char[len];
    sprintf(myfile_information.file_path, "/%s",get);
    myfile_information.file_path[len-1] = '\0';
    len = strlen(myfile_information.file_path);
    
    /*获取文件原来的名称*/
    int i = len;
    for(i = len-1; i>=0; i--)
    {
        if(myfile_information.file_path[i] == '/')
        {
            get = myfile_information.file_path + i + 1;
            break;
        }
    }
    len = strlen(get);
    strcpy(myfile_information.file_name,get);
    myfile_information.file_name[strlen(get)] = '\0';
    
    /*获取.*td文件名称*/
    len = strlen(myfile_information.file_name);
    for(int i=0; i<len; i++)
    {
        if(myfile_information.file_name[i]=='.')
        {
            myfile_information.file_name_td[i] = myfile_information.file_name[i];
            break;
        }
        myfile_information.file_name_td[i] = myfile_information.file_name[i];
    }
    sprintf(myfile_information.file_name_td, "%s*td",myfile_information.file_name_td);
    return HTTP;
}

/*发送HTTP请求头,接收HTTP响应头,对头部内容进行解析*/
void Baseclient :: parse_httphead()
{
    //cout << "发送HTTP请求头:\n";
    //cout << http_request << endl;
    //cout << "接收HTTP响应头:\n";
    int ret = write(sockfd, http_request, strlen(http_request));
    if(ret <= 0)
    {
        cout << "wrong http_request\n";
        exit(0);
    }
    int k = 0;
    char ch[1];
    /*解析出HTTP响应头部分*/
    while(read(sockfd, ch, 1) != 0)
    {
        http_respond[k] = ch[0];
        if(k>4 && http_respond[k]=='\n' && http_respond[k-1]=='\r' && http_respond[k-2]=='\n' && http_respond[k-3]=='\r')
        {
            break;
        }
        k++;
    }
    int len = strlen(http_respond);
    http_respond[len] = '\0';
    cout << http_respond<< endl;
    
    /*分析HTTP响应码*/
    HTTPCODE code;
    code = parse_HTTPCODE(http_respond);
    deal_with_code(code);
    
    /*解析出content-length:字段*/
    char *length;
    length = strstr(http_respond,"Content-Length:");
    if(length == NULL)
    {
        length = strstr(http_respond,"Content-length:");
        if(length == NULL)
        {
            length = strstr(http_respond, "content-Length:");
            if(length == NULL)
            {
                length = strstr(http_respond,"content-length:");
                if(length == NULL)
                {
                    cout << "NOT FOUND  Content-Length\n";
                    exit(0);
                }
            }
        }
    }
    char buf[10];
    char *get = strstr(length,"\r");
    *get = '\0';
    length = length + 16;;
    myfile_information.file_length = atol(length);
    int r_ret = read(sockfd,buf,1);
}

void* Baseclient :: work(void *arg)
{
    char *buffer;
    struct thread_package *my = (struct thread_package *)arg;
    
    /*设置套接字*/
    struct sockaddr_in client;
    struct hostent *thread_host;
    thread_host = gethostbyname(my->fqdn);
    client.sin_family = AF_INET;
    client.sin_addr.s_addr = *(int *)thread_host->h_addr_list[0];
    client.sin_port = htons(80);
    
    /*创建套接字*/
    my->sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(my->sockfd>=0);
    
    /*建立连接*/
    int ret = connect(my->sockfd, (struct sockaddr*)&client, sizeof(client));
    assert(ret != -1);
    //cout << "成功连接服务器!\n";
    //cout << "my->url:" << my->url << endl;
    /*填充HTTP GET方法的请求头*/
    char http_head_get[1000];
    sprintf(http_head_get,"GET %s HTTP/1.1\r\nHost: %s\r\nConnection: keep-alive\r\nRange: bytes=%ld-%ld\r\n\r\n",my->url, my->fqdn, my->start, my->end);
    // cout << "http_head_get:\n"  << http_head_get << endl;
    
    /*发送HTTP GET方法的请求头*/
    int r = write(my->sockfd, http_head_get, strlen(http_head_get));
    assert(r>0);
    //cout << "发送HTTP请求成功\n";
    /*处理HTTP请求头*/
    char c[1];
    char buf[2000];
    int k = 0;
    /*处理响应头函数,判断是否为HTTPS或者不合法HTTP响应头*/
    while(read(my->sockfd, c, 1) != 0)
    {
        buf[k] = c[0];
        if(k>4 && buf[k]=='\n' && buf[k-1]=='\r' && buf[k-2]=='\n' && buf[k-3]=='\r')
        {
            break;
        }
        k++;
    }
    int l = strlen(buf);
    buf[l] = '\0';
    cout << buf<< endl;
    HTTPCODE mycode = parse_HTTPCODE(buf);
    deal_with_code(mycode);
    
    int len = (my->end) - (my->start);
    buffer = new char[len];
    int fd = open(my->file_name, O_CREAT | O_WRONLY, S_IRWXG | S_IRWXO | S_IRWXU);
    assert(fd > 0);
    off_t offset;
    if((offset = lseek(fd, my->start, SEEK_SET)) < 0)
    {
        cout << "lseek is wrong!\n";
    }
    int ave = len;
    int r_ret = 0;
    int w_ret = 0;
    while((r_ret = read(my->sockfd, buffer, len))>0 && my->read_ret!=ave)
    {
        my->read_ret = my->read_ret + r_ret;
        len = ave - my->read_ret;
        w_ret = write(fd, buffer, r_ret);
        my->write_ret = my->write_ret + w_ret;
    }
    if(r_ret < 0)
    {
        cout << "read is wrong!\n";
    }
    delete [] buffer;
    close(fd);
    close(my->sockfd);
    return 0;
}
void Baseclient :: thread_download()
{
    void *statu;
    long int ave_bit;//线程平均字节数目
    struct thread_package *Thread_package;
    Thread_package = new struct thread_package[thread_number];
    
    /*如果.*td文件不存在,则为一个新的下载*/
    if(access(myfile_information.file_name_td, F_OK) != 0)
    {
        ave_bit = myfile_information.file_length / thread_number;
        
    }
    
    /*如果.*td文件存在,则属于断点下载*/
    else
    {
        int fd = open(myfile_information.file_name_td, O_CREAT | O_WRONLY, S_IRWXG | S_IRWXO | S_IRWXU);
        /*获取已经读过的文件大小*/
        long int file_size = get_file_size(fd);
        cout << "已经读取的字节数目:"<< file_size << endl;
        close(fd);
        /*计算出剩下的文件大小,计算每个线程应该读多少字节*/
        myfile_information.file_length = myfile_information.file_length - file_size;
        cout << "剩余字节数:" << myfile_information.file_length << endl;
        ave_bit = myfile_information.file_length / thread_number;
    }
    
    long int start = 0;
    int i = 0;
    /*多线程下载*/
    for(i=0; i<thread_number; i++)
    {
        Thread_package[i].read_ret = 0;//该线程已经从sockfd读取的字节数目
        Thread_package[i].write_ret = 0;//该线程已经写入文件的字节数目
        Thread_package[i].sockfd = -1;//该线程的socket
        Thread_package[i].start = start;//该线程读取文件内容的开始位置
        start = start + ave_bit;
        Thread_package[i].end = start;//该线程读取文件内容的结束位置
        Thread_package[i].fqdn = fqdn;//该线程存取访问的fqdn
        Thread_package[i].url = address_buf;//该线程存取下载地址
        strcpy(Thread_package[i].file_name, myfile_information.file_name_td);//该线程存取文件名称CIF文件,以判断是否为断点下载
    }
    int Sum = 0;
    for(i=0; i<thread_number; i++)
    {
        /*pthread_create(&pid, NULL, work, &Thread_package[i]);
         pthread_join(pid, &statu);*/
        pthread_create(&Thread_package[i].pid, NULL, work, &Thread_package[i]);
        pthread_detach(Thread_package[i].pid);
    }
    
    /*打印进度条*/
    cout << "打印进度条\n";
    char bar[120];
    char lable[4]="/|\\";
    int k=0;
    int count = 0;
    /*主线程反复循环,查看各线程是否完成下载,若所有线程完成下载,则退出循环*/
    while(1)
    {
        count = 0;
        for(auto i=0; i<thread_number; i++)
        {
            count = count + Thread_package[i].write_ret;
        }
        /*按照百分比打印下载进度条*/
        double percent = ((double)count / (double)myfile_information.file_length)*100;
        while(k <= (int)percent)
        {
            printf("[%-100s][%d%%][%c]\r", bar, (int)percent, lable[k % 4]);
            fflush(stdout);
            bar[k] = '#';
            k++;
            bar[k] = 0;
            usleep(10000);
        }
        if(count == myfile_information.file_length)
        {
            cout << "\n下载结束\n";
            break;
        }
    }
    if(count != myfile_information.file_length)
    {
        int r = remove(myfile_information.file_name_td);
        if(r == 0)
        {
            cout << "下载失败!\n";
        }
        exit(0);
    }
    else{
        rename(myfile_information.file_name_td, myfile_information.file_name);
        cout << "下载成功!\n";
    }
    
}
void Baseclient :: mysocket()
{
    STATUS mystatu;
    int len = strlen(address);
    address_buf = new char[len];
    strcpy(address_buf, address);
    
    mystatu = parse_address();//解析输入的下载地址,仅仅支持HTTP下载
    if(mystatu==HTTPS)
    {
        cout << "该程序仅支持HTTP下载\n";
        exit(0);
    }
    if(host == NULL)
    {
        cout << "无法解析FQDN的IP地址,请检查下载地址是否输入正确\n";
        exit(0);
    }
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = *(int *)host->h_addr_list[0];
    server.sin_port = htons(port);
    
    /*创建套接字*/
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd>=0);
    
    /*创建连接*/
    int ret = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
    assert(ret != -1);
    cout << "成功连接服务器!\n";
    
    /*填充HTTP请求头*/
    sprintf(http_request,"HEAD %s HTTP/1.1\r\nHost: %s\r\nConnection: Close\r\n\r\n ",address_buf ,fqdn);
    //cout << "http_request:\n" << http_request << endl;
    
    /*分析收到的HTTP响应头*/
    parse_httphead();
    
    /*根据线程数量进行下载文件*/
    thread_download();
    
}

download_main.cpp

#include"download.h"
void menu()
{
    int thread_number;
    string address;
    cout << "请输入下载地址:" << " ";
    cin >> address;
    int len = address.length();
    char *add;
    add = new char [len+1];
    address.copy(add, len, 0);
    add[len] = '\0';
    cout << "add:" << add << endl;
    cout << "请输入线程数量:\n";
    cin >> thread_number;
    if(thread_number>20 || thread_number<=0)
    {
        cout << "该线程数量不符合范围,已使用默认线程数量下载\n";
        thread_number = 5;
    }
    Baseclient myclient(thread_number, add);
    myclient.mysocket();
    
}
int main(int argc, char const *argv[])
{
    menu();
    return 0;
}

可扩展功能

  1. 更改下载文件名称
  2. 指定下载路径

总结

感觉现在基本没有人会用HTTP协议去下载东西了,除了一些小一点的图片还可以HTTP下载以外,基本没有啥可以下载的东西了。所以感觉写这个东西,感觉意义不大,唯一有意义的就是可以感受到下载客户端多线程怎么去下载的,怎么进行断点下载,怎么去发送下载请求。。除此之外还理解了为什么多线程下载速度会快,但是也不是任何场景多线程下载都是比单线程快的,多线程需要叠加起来速度才会快。还有下载文件的时候,每个线程应该单独把自己的部分写入自己的一个文件里,最后去合并内容,这样比较靠谱。如果都在线程函数里,就直接写入部分的话是不够稳妥的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值