写完个人感觉,没有啥实用意义是因为想从服务器上下载的东西基本都是支持HTTPS协议下载的,并非HTTP了。
实现功能
- 多线程下载
- 断点下载
- 进度条显示
具备知识点
- HTTP请求以及响应头
- 多线程下载
实现思路
- 根据下载地址(仅仅限于HTTP下载)来解析下载地址中的FQDN以及指定的端口号,通过函数gethostbyname获取服务器的ip地址,通过套接字地址结构的填充连接上服务器。
- 此时通过发送HTTP请求头,尝试是否可以连接上服务器,发送HEAD请求方法,请求HTTP的响应头,通过响应头的content-length关键字,获取资源文件的大小。
- 此时检查文件是否属于断点下载,其中判断是否为断点文件下载的标准是定义了.*td文件,在下载过程未结束的过程中,都是写入.*td文件中,直到下载结束将文件名称的后缀名改成之前指定的名称格式。所以.*td文件存在则说明是断点下载,否则为一个新的下载。其中若下载过程中失败的话,则删除.*td文件
- 根据用户制定的线程数量进行下载,其中线程数量不能超过20,若超过二十则默认为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;
}
可扩展功能
- 更改下载文件名称
- 指定下载路径
总结
感觉现在基本没有人会用HTTP协议去下载东西了,除了一些小一点的图片还可以HTTP下载以外,基本没有啥可以下载的东西了。所以感觉写这个东西,感觉意义不大,唯一有意义的就是可以感受到下载客户端多线程怎么去下载的,怎么进行断点下载,怎么去发送下载请求。。除此之外还理解了为什么多线程下载速度会快,但是也不是任何场景多线程下载都是比单线程快的,多线程需要叠加起来速度才会快。还有下载文件的时候,每个线程应该单独把自己的部分写入自己的一个文件里,最后去合并内容,这样比较靠谱。如果都在线程函数里,就直接写入部分的话是不够稳妥的。