原文在我的个人网站上:http://www.vicking.pw/?p=155
C 语言也能干大事啊,下面是我写的一个运行在linux下的多线程下载并有断点续传的功能的程序,花了三天时间,不吃不喝,累死我了。不过终于写出来了,一些基本的bug已经去除了。然后其他的问题希望大家多多测试哈,问题发送到我的邮箱来:[email protected]
多线程下载 并支持 断点续传 程序(类unix下终端程序)
名称:mwget
版本:V0.1
开发者:Vicking
开发周期:2018-3-22至2018-3-25
github地址: https://github.com/shengliwang/mwget
使用方法
./mwget URL [-t 线程数] [-o 保存的文件名] 如果不指定线程数则使用单线程,不指定文件名,则使用服务器端的名字
软件的一个截屏:
一、开发总体思路如下:
1,根据用户指定的URI来分析文件大小。
2,根据用户指定的线程数来创建线程分段下载整个文件,最后合并起来。
3,需要指示出下载进度,下载完成时所用的时间
4,需要指定本程序应用的范围:如https不可下载
二、程序编写及运行具体过程
1,包含相应的头文件
2,定义宏来指定程序版本, 作者
3, main函数
4,分析用户指定参数(如果条件不合适,则打印出错信息并退出):
创建一个结构体cmd用来保存初始化后的参数
分析函数应找出使用的为http协议,其他协议目前不支持,应给出提示信息并退出
分析函数提取server的FQDN名称转化成整形的ipv4地址,ipv6地址目前不支持,如果为ipv6,给出信息并退出
分析函数应提取server的端口号,如果没有指定端口,则使用默认端口80
分析函数应提取获取文件的名字,如果没有名字,则默认获取index.html并给出提信息
分析函数应提取下载文件另存为的文件名字,如果没有,则用上面的sever端名字
分析函数应提取下载所用的线程数,如果没有指定,则使用单线程下载,限定使用下载线程数不能超过定义的MAX_THREAD_NUM宏(util.c)
5,根据分析用户指定的参数(命令行参数)获取到了server端的ip地址和端口号,以及要下载的文件的长度信息,和使用的线程数
创建一个函数, 此函数应先发出HTTP 的 HEAD报文获取要获得的文件长度 (顺便测试服务器能不能正常连上,不行的话则报错退出 . 1)、先比较状态码,不正确则报错退出 2)、然后找长度,找不到,则退出并报错)
6, 上面初始化工作结束后,下载之前应输出如下信息,此程序名称,版本号,开发者,使用线程数,server端信息,下载文件名称,存储文件名称,
7,创建多个线程进行下载 (通过cfg配置文件进行线程间通讯,其中包含完成进度信息)
每个线程中应有一个变量用来表示线程完成的进度(创建线程的时候传入),每个线程完成一次read和write后对这个变量进行增加
在主线程中实现一个函数,用来检测这些变量的值,然后跟文件总长度做比较,给出进度条
8,测试是否进行断点续传
1, cfg 文件保存未完成下载的download文件的一些信息(如线程个数,已完成的长度),用于断点续传
1, 创建通用的线程函数,用于重头下载和续传的两种模式,只要提交给他任务就行
2, 检测是否断点续传的情况 (创建函数进行测试)
1)、 download文件不存在,重头下载。
2)、检测到download文件存在,cfg文件不存在,重新下载,并提示用户是否覆盖download文件,不覆盖则退出。
3)、检测到download文件存在,cfg存在,则判断cfg文件能用否(cfg文件以yes开头能用,并且起大小是某个信息结构体的整数倍,程序中有说明),
1>>能用,则判断download文件大小是否正确,不正确提示覆盖,重头下载,正确则续传
2>>不能用,报错,并重头下载,并且覆盖cfg文件。
10, 回收线程:
可以根据线程退出值来判断下载线程有没有执行成功,如果不能执行成功(下载线程返回非NULL值),则整个进程退出,并提示用户重新启动本程序,断点续传。
遇到的问题
就是在线程函数发送请求时,收不到服务器的回应,用tcpdump抓包发现,发送的报文不完全,原来是request的的长度太短了(原来为128)。改长后就正常了。(如下抓的报部分)
数据请求部分
192.168.2.102.52142 > 101.6.8.193.80: Flags [P.], cksum 0x317c (incorrect -> 0xa6a5), seq 0:128, ack 1, win 229, options [nop,nop,TS val 3963801377 ecr 3603563775], length 128: HTTP, length: 128
GET /centos/7.4.1708/isos/x86_64/CentOS-7-x86_64-NetInstall-1708.iso HTTP/1.1
Range: bytes=0-442499071
Host: mirrors.tuna.tsin[!http] // 发送的请求到此结束(粗体),可见没有发送完全
//对比程序中对rquest的构建,request一共128字节,sprintf导致部分数据没有被request数组保存,所以把request调大即可
首先放上makefile(# 注意: 每个编译都加了-g,代表是debug版本,如果想要release版本,把每个编译的-g 去除即可)(另外这是一个非常简单非常low的写法,哈哈)
all:wrap/wrap.o main.o util.o test_continue.o thread_download.o print_scheduler.o thread_join.o
gcc debug/wrap.o debug/main.o debug/util.o debug/test_continue.o \
debug/thread_download.o debug/print_scheduler.o debug/thread_join.o -g -pthread -o debug/mwget
thread_join.o:thread_join.c
gcc thread_join.c -c -g -pthread -o debug/thread_join.o
print_scheduler.o:print_scheduler.c
gcc print_scheduler.c -c -g -o debug/print_scheduler.o
wrap/wrap.o: wrap/wrap.c
gcc wrap/wrap.c -c -g -o debug/wrap.o
main.o: main.c
gcc main.c -c -g -o debug/main.o
util.o: util.c
gcc util.c -c -g -o debug/util.o
test_continue.o: test_continue.c
gcc test_continue.c -c -g -o debug/test_continue.o
thread_download.o: thread_download.c
gcc thread_download.c -c -g -o debug/thread_download.o
clean:
rm -f debug/*
rm -f debug/.*.cfg
下面一步一步来讲解本程序的工作原理
main.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "wrap/wrap.h"
#include "util.h"
#define VERSION "V0.1" // 版本号
#define AUTHOR "Vicking" //作者
ssize_t length = 0; //要下载文件的大小 定义成全局变量,一会有用
unsigned short thred_num = 0; //全局变量,用于线程join
void * moloc = NULL; //全局变量,用于线程join
void * map = NULL; //全局变量,用于线程join
size_t map_len = 0;
// 上面四个变量用于线程回收,在thread_join函数中用到,此函数在thread_join.c 文件中实现
void err_exit(char *s )
{
perror(s);
exit(9);
}
int
main(int argc, char * argv[])
{
//打印欢迎信息
printf( "\n\033[31mWelcome use mwget(multi download program)\n"
"Version: %s. Author: %s\n\033[0m", VERSION, AUTHOR);
struct cmd options;
init_args(argc, argv, &options); // 初始化参数,如果参数错误,则打印出错信息并退出,此函数在util.c 文件中实现
if(strcmp(options.protocol, "http") != 0)
{
printf("\"%s\" protocol is not supported\n", options.protocol);
exit(2);
}
//由FQDN 获得server的ip地址 为网络型大段存放的4个字节: host.h_addr_list
struct hostent * host;
host = gethostbyname(options.serv_name);
if(NULL == host)
{
printf("server not found: %s\n", options.serv_name);
exit(2);
}
//从server端获取文件长度,顺便测试服务器能不能连接上
char head[1024] = {0};
bzero(head, sizeof(head));
length = get_file_length(options.protocol, options.serv_name,
options.serv_port, options.file_name, head, sizeof(head));
char down_file[1024] = {0};
char cfg_file[1024] = {0};
sprintf(down_file, "%s.mdownload", options.save_name);
sprintf(cfg_file, ".%s.cfg", options.save_name);
// 创建对配置文件进行mmap的地址
char * cfg = NULL;
struct task * tsk;
pthread_t * tid = NULL;//用于回收线程
if(test_continue(down_file, cfg_file, length) == false)// 检测是否进行续传,此函数在test_continue.c 中实现
{
printf("\033[33mnow download file:\033[0m %s\n\n", down_file);
// 不能续传
// 1, 先删除down 文件和cfg文件
if(access(down_file, F_OK) == 0)
if(unlink(down_file) != 0)
{
perror(down_file);
exit(7);
}
if(access(cfg_file, F_OK) == 0)
if(unlink(cfg_file) != 0)
{
perror(cfg_file);
exit(7);
}
// 2, 创建cfg文件,并且mmap到内存空间
//
int cfg_fd;
off_t off_set;
if( (cfg_fd = open(cfg_file, O_RDWR | O_CREAT, 0644)) < 0)
err_exit(cfg_file);
if(write(cfg_fd, "yes", 3) < 0)
err_exit(cfg_file); //前三个字节写入yes用来标示这个配置文件可用
ssize_t cfg_len = sizeof(struct task) * options.thread_num + 3;
if( (off_set = lseek(cfg_fd, cfg_len-1, SEEK_SET) < 0) )
err_exit("lseek");
if(write(cfg_fd, "\0", 1) < 0) // 上面的lseek和这句write用来拓展这个文件
err_exit(cfg_file);
// 创建并拓展down file
int downfd = open(down_file, O_WRONLY | O_CREAT, 0644);
if(downfd < 0)
err_exit("create download file");
if( lseek(downfd, length-1, SEEK_SET) < 0)
err_exit("lseek down file");
Write(downfd, "\0", 1);
Close(downfd);
cfg = (char *)mmap(NULL, cfg_len, PROT_READ | PROT_WRITE, MAP_SHARED, cfg_fd, 0);
map = (void *)cfg;
map_len = cfg_len;
// mmap的内存在主程序结束后别忘释放
if(cfg == NULL)
err_exit("mmap");
close(cfg_fd);
cfg += 3; // 把指针往后移动三位
tsk = (struct task *)cfg; // 把char *类型强转并且把地址值赋值到struct task *类型的tsk中
// 3, 把task结构体进行任务分配
struct sockaddr_in servaddr; //创建服务器的ip地址和端口
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = *(int *)host->h_addr_list[0];
servaddr.sin_port = htons(atoi(options.serv_port));
ssize_t total = length;
ssize_t step = length / options.thread_num;
ssize_t end = 0;
ssize_t offset = 0;
// 对于如何分配每个线程所用的参数
// end 成员前几个都是step的整数倍,而最后一个时total长度
for(int i = 0; i < options.thread_num; i++)
{
tsk[i].addr = servaddr;
tsk[i].total = total;
if(i != (options.thread_num -1) )
tsk[i].end = (i+1) * step -1;
else
tsk[i].end = total -1;
tsk[i].start = i * step;
tsk[i].offset = 0;
strcpy(tsk[i].serv_file, options.file_name);
strcpy(tsk[i].save_file, down_file);
strcpy(tsk[i].server, options.serv_name);
strcpy(tsk[i].server_port, options.serv_port);
}
// 创建线程进行下载
// 2, 创建多个线程
tid = (pthread_t *)malloc(sizeof(pthread_t) * options.thread_num);
moloc = (void *)tid;
thred_num = options.thread_num;
for(int i = 0; i < options.thread_num; i++)
{
pthread_create(&tid[i], NULL, thread_download, (void *)&tsk[i]);//线程下载函数thread_download 在 thread_download.c中实现
}
pthread_t tmp;
pthread_create(&tmp, NULL, thread_join, (void *)tid); //创建用于回收下载线程的线程
pthread_detach(tmp);
// 下面开始进行重要的一项,打印下载速度,打印进度
print_scheduler(tsk, options.thread_num);
//此函数在print_scheduler.c 中实现,由主线程实现打印(即本线程)
pthread_cancel(tmp); //
if(cfg != NULL) // munmap 刚才映射的cfg文件
munmap(cfg, cfg_len);
}else
{
// 可以续传
//1, mmap cfg 文件到内存
printf("\033[33mnow continue download file:\033[0m %s\n\n", down_file);
int cfg_fd;
off_t off_set;
if( (cfg_fd = open(cfg_file, O_RDWR)) < 0)
err_exit("cfg_file in invalid, please delet it");
struct stat st;
stat(cfg_file, &st);
options.thread_num = st.st_size / sizeof(struct task);
cfg = (char *)mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, cfg_fd, 0);
map = (void *)cfg;
map_len = st.st_size;
if(cfg == NULL)
err_exit("mmap cfg_file");
Close(cfg_fd);
cfg += 3; // 把指针往后移动三位
tsk = (struct task *)cfg; // 把char *类型强转并且把地址值赋值到struct task *类型的tsk中
// 创建线程进行下载
// 2, 创建多个线程
thred_num = options.thread_num;
tid = (pthread_t *)malloc(sizeof(pthread_t) * options.thread_num);
moloc = (void *)tid;
for(int i = 0; i < options.thread_num; i++)
{
pthread_create(&tid[i], NULL, thread_download, (void *)&tsk[i]);
}
pthread_t tmp;
pthread_create(&tmp, NULL, thread_join, (void *)tid);//创建用于回收下载线程的线程
pthread_detach(tmp);
// 下面开始进行重要的一项,打印下载速度,打印进度
print_scheduler(tsk, options.thread_num);
pthread_cancel(tmp); // 因为那个用于回收的线程没有退出的地方,所以cancel掉
if(cfg != NULL) // munmap 刚才映射的cfg文件
munmap(cfg, st.st_size);
}
//主线成退出之前把cfg文件删除,把文件名修改了
// 1, 删除cfg文件
if(access(cfg_file, F_OK) == 0)
unlink(cfg_file);
// 2,修改down文件的名字
if( rename(down_file, options.save_name) < 0)
perror("rename failed, please rename by hand!");
return 0;
}
util.c (里面包含初始化函数: init_args 和获取文件长度的函数get_file_length)
/* util.c */
#include
#include
#include
#include
#include
#include
#include "util.h"
#define MAX_THREAD_NUM 8 //定义最大线程数
#define USAGE "Usage: mwget [-t thread_num(1~%d)] [-o save_file_name] URL\nFor example: mwget -t 4 -o hello.html http://www.baidu.com/index.html\n"
void usage_exit() // 打印用法并退出
{
printf(USAGE, MAX_THREAD_NUM);
exit(1);
}
void get_name_from_server(char * local_file, const char * server_file)
{
//从server_file这样的字串中"***/*****/****/yyyy.xxx"提取yyyy.xxx保存到local_file 中去
//我认为下面是非常好的手段
char * tmp = NULL;
while(1)
{
if( (tmp = strstr(server_file, "/")) != NULL )
{
server_file = tmp + 1;
continue;
}
else
{
strcpy(local_file, server_file);
break;
}
}
}
void init_args(int argc, char * argv[], struct cmd * options)
{
// 如果没有命令行参数,打印用法并退出
if(1 == argc)
usage_exit();
bzero(options, sizeof(struct cmd));
argv ++; // argv[0] 省略,直接从argv[1] 开始遍历
char url[4096]; // 用于分析URL
while(*argv)
{
// 找到 -t 后面跟的线程数
if(strcmp("-t", *argv) == 0)
{
if(argv[1] == NULL)
usage_exit();
options->thread_num = strtol(argv[1], NULL, 10);
if(options->thread_num == LONG_MIN || options->thread_num == LONG_MAX)
{
perror("-t num");
usage_exit();
}
if(options->thread_num > MAX_THREAD_NUM || options->thread_num <= 0)
usage_exit();
argv += 2; // 二级指针往后移动两下
continue;
}
// 解析要存储文件的名字
else if(strcmp("-o", *argv) == 0)
{
if(argv[1] == NULL)
usage_exit();
strcpy(options->save_name, argv[1]);
argv += 2; // 二级指针往后移动两下
continue;
}
// 解析server端的FQDN名称, 端口号,以及 要下载的文件的名字, 和协议
// 排除ftp和https
else if(strstr(*argv, "://"))
{
// 获取协议
bzero(url, sizeof(url));
strcpy(url, *argv);
strtok(url, "://");
strcpy(options->protocol, url);
// 获取server FQDN名称
bzero(url, sizeof(url));
strcpy(url, *argv);
int i = strlen(options->protocol)+3;
for(int j = 0; i < strlen(url); i++)
{
if(url[i] != ':' && url[i] != '/')
{
options->serv_name[j] = url[i];
j++;
}
else
break;
}
if(strlen(url) == i+1)
{
argv ++;
continue;
}
// 获取server 的端口号
if( url[i] == ':'){
i++;
for(int j = 0; i < strlen(url); i++)
{
if(url[i] != '/')
{
options->serv_port[j] = url[i];
j ++;
}
else
break;
}
}
if(strlen(url) == i+1)
{
argv ++;
continue;
}
// 获取要下载的文件在服务器的路径
if( url[i] == '/')
{
i++;
for(int j = 0; i < strlen(url); i++)
{
options->file_name[j] = url[i];
j ++;
}
}
argv++;
continue;
}
else
usage_exit(); // 解析到不认识的参数,则提示错误并退出
}
// 给选项赋给默认值
if(strlen(options->serv_port) == 0)
strcpy(options->serv_port, "80"); // 默认端口 80
if(strlen(options->file_name) == 0)
strcpy(options->file_name, "index.html"); // 默认获取文件名为 index.html
if(options->thread_num == 0)
options->thread_num = 1; // 默认线程个数为 1
if(strlen(options->save_name) == 0)
get_name_from_server(options->save_name, options->file_name); // 如果本地保存文件名不指定,则用此文件在server端的名字 注意:server端的名字可能带有'/', 所以要处理以下
// 检查错误
if(strlen(options->serv_name) == 0)
usage_exit(); //如果不能解析到服务器FQDN名称,则提示退出
}
// 由已知的协议,服务器FQDN地址,端口,文件名,获server端的文件大小
ssize_t get_file_length(
char * protocol, char * fqdn, char * port, char * fname,
char * head, size_t headlen)
{
struct hostent * host;
host = gethostbyname(fqdn);
if(NULL == host)
{
printf("server not found: %s\n", fqdn);
exit(2);
}
if(host->h_addrtype != AF_INET)
{
printf("%s: only ipv4 supported\n", fqdn);
exit(2);
}
// 创建套接字
int sockfd = Socket(AF_INET, SOCK_STREAM, 0);
// 构建server的 addr
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = *(int *)host->h_addr_list[0];
servaddr.sin_port = htons(atoi(port));
// connect to server
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
// 给服务器发送一个 HEAD 类型的请求。
//1, 先构建请求报文
char request[1024];
bzero(request, sizeof(request));
sprintf(request,"HEAD /%s HTTP/1.1\r\n"
"Host: %s:%s\r\n"
"Connection: Close\r\n\r\n", fname, fqdn, port); //构建的报文,让链接不是Keep-Alive
//而是 Close状态
//2, 发送报文
Write(sockfd, request, strlen(request));
printf("HTTP request is sent, waiting echo ......\n");
// 接收服务器发来的回应报文
char * buf = request; // 用于接收服务器发来信息的buf
bzero(buf, sizeof(request));
Read(sockfd, buf, sizeof(request));
Close(sockfd);
printf("HTTP echo is recived:\n\033[1m%s\033[0m", buf);
if( (NULL != head) && strlen(buf) < headlen)
strcpy(head, buf);
// 分析从服务器发过来的内容,分析长度还有其他信息。
// 信息存储在buf中
// 简单说一下http回复报文的结构
// 第一行 协议/版本号 状态码 文字描述 如: HTTP/1.1 200 OK
// 第二行到多行 消息报头类型: 内容 Content-Length: 761 \r\n
// ....... Connection: Close \r\n
// 上面是报文头部,头部与正文之间以\r\n 隔开 \r\n
// 这里为正文部分 正文
// 其中 状态码在 >= 200 和 < 400 之间是 正常可用的,遇到其他的状态码应打印出错信息退出
//获取状态码
int i = 0;
char status[8] = {0};
for(; i < strlen(buf); i++) // 此循环可获取HTTP协议版本号
{
if(buf[i] == ' ')
{
i++;
break;
}
}
for(int j = 0; i < strlen(buf); i++) // 此循环用来获取状态码
{
if(buf[i] != ' ')
{
status[j] = buf[i];
j++;
}
else
break;
}
int stat_num = atoi(status);
if( ((stat_num / 100) != 2) && ((stat_num / 100) != 3) ) //判断当状态码不再200-399之间的时候出错退出
{
printf("error: status: %d\n", stat_num);
exit(2);
}
// 解析文件长度
// 1, 先获取含有 “Content-Length“的字串
char * file_len;
if( (file_len = strstr(buf, "content-length")) != NULL)
;
else if( (file_len = strstr(buf, "content-Length")) != NULL)
;
else if( (file_len = strstr(buf, "Content-length")) != NULL)
;
else if( (file_len = strstr(buf, "Content-Length")) != NULL)
;
else
{
printf("content-length not found\n");
exit(2);
}
// 2, 从该字串中提取文件长度信息
file_len = &file_len[15]; //把file_len 的指针移到之后,含数字的字符串之前
char tmp[100] = {0}; //用于保存长度的字串
for(int j = 0, t = 0; file_len[j] != '\r'; j++)
{
if(file_len[j] != ' ')
{
tmp[t] = file_len[j];
t++;
}
}
return strtol(tmp, NULL, 10);
}
test_continue.c 测试是否进行续传的函数
#include "util.h"
#include
#include
#include
#include
#include
#include
#include
#include "wrap/wrap.h"
bool read_opt(char * prompt)
{
char opt[10];
printf("%s", prompt);
fflush(stdout);
bzero(opt, sizeof(opt));
Read(STDIN_FILENO, opt, sizeof(opt));
//对用户的答案进行判断
//1, y\n 2, n\n 3, ****\n 4, \n
while(1)
{
if(strcmp("y\n", opt) == 0)
return true;
else if(strcmp("n\n", opt) == 0)
return false;
else if(strcmp("\n", opt) == 0)
return true;
else
{
printf("invalid answer! %s", prompt);
bzero(opt, sizeof(opt));
Read(STDIN_FILENO, opt, sizeof(opt));
}
}
}
bool test_continue(char * down_file, char * cfg_file, ssize_t file_length)
{
char prompt[512];
struct stat st;
char tmp[3];
if(access(down_file, F_OK) != 0)
return false; // down file 不存在,返回假, 代表要重新下载
else if(access(cfg_file, F_OK) != 0)
{
// down file 存在 但cfg文件不存在, 提示是否覆盖down文件
bzero(prompt, sizeof(prompt));
sprintf(prompt, "%s file not exists, overwrite %s? [y]/n: ", cfg_file, down_file);
if(read_opt(prompt) == true)
return false;
else
exit(5);// 如果不覆盖,直接从这退出
} else {
// cfg文件存在, down file 也存在, 此时需要判断down file 的大小是否正确
stat(down_file, &st);
if(st.st_size != file_length)
{
// down file大小不正确,判断用户答案,确定覆盖,程序返回假,重新下载,选择“不”,
// 程序直接从这个函数退出
bzero(prompt, sizeof(prompt));
sprintf(prompt, "%s size if not right, overwrite it? [y]/n: ", down_file);
if(read_opt(prompt) == true)
return false;
else
exit(5);
}
else
{
// down file 大小正确,这时判断cfg文件能否可用,能用的标志时,前三个字节为"yes"
// 能用的另一个标志是,大小是结构体的整数倍并且多三个字节
int fd = open(cfg_file, O_RDONLY);
if(fd < 0)
{
perror("open cfg_file");
return false;
}
if (read(fd, tmp, 3) < 0)
{
perror("read cfg_file");
return false;
}
close(fd);
if(strcmp(tmp, "yes") == 0)
{
struct stat st;
if( stat(cfg_file, &st) < 0)
{
perror("stat cfg_file");
return false;
}
if( (st.st_size % sizeof(struct task)) == 3 ) //文件大小是结构体的整数倍余3,能用
return true;
else
return false;
}
else
return false;
}
}
}
thread_download.c : 里面由用于下载的线程函数
#include "util.h"
#include
#include
#include
#include "wrap/wrap.h"
#include
#include
#include
#include
#include
#include
#define RECV_BUF 8192 //定义每次接收的buf长度
void sys_exit(char * s) //非正常退出的函数,退出值为1 用于线程回收用
{
perror(s);
pthread_exit((void *)1);
}
//此函数找到报文的数据部分,并且把要写入文件的长度计算出来(把报头和数据分开)
char * get_data(char * s, size_t * len) //len为传入传出参数
{
char * seg = strstr(s, "\r\n\r\n");
if(seg == NULL)
return s;
int i;
for(i = 0; i < *len; i++)
{
if(s[i] == '\r')
{
if(s[i+1] == '\n')
{
if(s[i+2] == '\r')
{
if(s[i+3] == '\n')
break;
else
continue;
}
else
continue;
}
else
continue;
}
else
continue;
}
//测试用,在函数返回之前把报文打印出来
// printf("------------------thread: server echo (head)----------------\n");
// write(STDOUT_FILENO, s, i+4);
*len = *len - (i + 4); // len-i-4 为数据长度,i+4为报文长度
seg += 4;
return seg;
}
void close_exit(int connfd) //正常退出的函数,退出值为null
{
Close(connfd);
pthread_exit(NULL);
}
void *
thread_download(void * arg)
{
struct task * task = (struct task *)arg;
// 创建套接字
int connfd = Socket(AF_INET, SOCK_STREAM, 0);
// 链接服务器
Connect(connfd, (struct sockaddr *)&task->addr, sizeof(task->addr));
// 发送报文
// 1,创建报文
char request[8192] = { 0}; // request 的大小尽量调大一点,不然报文有可能发送不完全
sprintf(request, "GET /%s HTTP/1.1\r\n"
"Range: bytes=%ld-%ld\r\n"
"Accept: */*\r\n"
"User-Agent: mwget/0.1(mutithread download program on linux) "
"developed by Vicking(xixi: www.vicking.pw)\r\n"
"Accept-Encoding: identity\r\n"
"Connection: close\r\n" // 只要服务器发送完数据,要求他主动断开,这样线程就能len返回0, 然后断开链接
"Host: %s:%s\r\n\r\n"
, task->serv_file
, task->start + task->offset, task->end
, task->server, task->server_port);
// 2, 发送报文
Write(connfd, request, sizeof(request));
ssize_t len;
char buf[RECV_BUF];
char err[128];
// 3, 接收报文 并且写入到文件中
int savefd = open(task->save_file, O_WRONLY | O_CREAT, 0644);
if(savefd < 0)
{
bzero(err, sizeof(err));
sprintf(err, "open: %s", task->save_file);
sys_exit(err);//非正常退出用的函数,表示线程退出值非NULL 用于线程回收
}
off_t offset;
if( (offset = lseek(savefd, task->start + task->offset, SEEK_SET) < 0) ) //把指针移到相应的位置上
sys_exit("thread: lseek");
// 1》第一次接收数据舍并弃报头
bzero(buf, sizeof(buf));
// printf("---------------------ready to reve first data-----------------------\n");
len = Read(connfd, buf, sizeof(buf));
// printf("---------------------rcve first data down---------------------\n");
if(len == 0)
close_exit(connfd); //正常关闭用的函数
else{
char * data = get_data(buf, &len);
task->offset += Write(savefd, data, len); // 每写入部分字节,就把offset的值增加
}
// 2》 后来收的数据全部存到文件中去
while(1)
{
bzero(buf, sizeof(buf));
len = Read(connfd, buf, sizeof(buf));
if(len == 0)
close_exit(connfd);
else
task->offset += Write(savefd, buf, len);
}
}
print_scheduler.c : 用于打印进度条的函数实现
#include
#include
#include "util.h"
#include
#include
#include
#include
#define TIME_INTER 700000 // 定义打印进度条的间隔时间, 现在是0.7s
void set_file_str(char * print, size_t len, const char * src) //实现能滚动的文件名
{
if(strlen(src) <= len)
{
strcpy(print, src);
for(int i = strlen(src); i < len; i ++)
print[i] = ' ';
return ;
}
static int i = 0; //此函数每次进来 i 值都不一样
int index = i % (strlen(src) - len);
for(int j = 0; j < len; j++)
{
print[j] = src[index];
index ++;
}
i ++;
}
void set_size_str(char * str, ssize_t size) // 大小占用8个字节
{
if((size / 1024) == 0) //条件成立的话用B作为单位
sprintf(str, "%7.2fB", (double)size);
else if((size/1024/1024) == 0) // 用KB作为单位
sprintf(str, "%7.2fKB", (double)size / 1024);
else if((size/1024/1024/1024) == 0) // 用MB作为单位
sprintf(str, "%7.2fMB", (double)size/1024/1024);
else if((size/1024/1024/1024/1024) == 0) // 用GB作为单位
sprintf(str, "%7.2fGB", (double)size/1024/1024/1024);
else if((size/1024/1024/1024/1024/1024) == 0) // 用TB作为单位
sprintf(str, "%7.2fTB", (double)size/1024/1024/1024/1024);
else
return;
}
void set_speed_str(char * str, ssize_t now)
{
static ssize_t before = 0;
ssize_t size = (long)((double)(now - before) / (TIME_INTER/(double)1000000));
if((size / 1024) == 0) //条件成立的话用B作为单位
sprintf(str, "%6.2fB/s", (double)size);
else if((size/1024/1024) == 0) // 用KB作为单位
sprintf(str, "%6.2fKB/s", (double)size / 1024);
else if((size/1024/1024/1024) == 0) // 用MB作为单位
sprintf(str, "%6.2fMB/s", (double)size/1024/1024);
else if((size/1024/1024/1024/1024) == 0) // 用GB作为单位
sprintf(str, "%6.2fGB/s", (double)size/1024/1024/1024);
else if((size/1024/1024/1024/1024/1024) == 0) // 用TB作为单位
sprintf(str, "%6.2fTB/s", (double)size/1024/1024/1024/1024);
before = now;
return;
}
void set_bar_str(char * str, ssize_t size, ssize_t done, ssize_t total)
{
if(size <= 10)
return;
int bar_len = size - 6; //进度条的长度为size减去其他的东西([] 和百分数)
sprintf(str, "[%3ld%%", done*100/total);
str += 5;
int i;
for(i = 0; i < (done * bar_len / total); i++)
str[i] = '=';
str[i-1] = '>';
for(; i < bar_len; i++)
str[i] = ' ';
str[bar_len] = ']';
return ;
}
void print_scheduler(const struct task * tsk, unsigned short thread_num)
{
ssize_t total = tsk->total; //总的字节数
ssize_t done = 0; //已完成的字节数
//进度条组成字符
char file[21]; //最大20个字符表示文件
char geted[10]; // 表示已经完成的字节数的字符表示 最大长度为9如:1000.96MB
char speed[12]; // 表示 下载速度的字节 最大长度为12 如: 1000.96KB/s
char bar[200]; // 表示进度条的最大长度
struct winsize win;
unsigned short width = 0; //窗口的宽度(以字符计)
//如果窗口宽度小于60 则只打印进度条
while(1)
{
done = 0;
for(int i = 0; i < thread_num; i++) //获取已完成的字节数
{
done += tsk[i].offset;
}
//1, 实现滚动的文件名
bzero(file, sizeof(file));
set_file_str(file, sizeof(file) - 1, tsk->save_file); //每次循环文件名都不一样(能滚动)
//2, 实现下载内容下载了多少
bzero(geted, sizeof(geted));
set_size_str(geted, done);
//3, 下载速度获取 // 不是很精确,因为没有考虑程序运行的时间
bzero(speed, sizeof(speed));
set_speed_str(speed, done);
//4, 进度条的获取
ioctl(STDIN_FILENO, TIOCGWINSZ, &win); //获取窗口大小
width = win.ws_col;
bzero(bar, sizeof(bar));
set_bar_str(bar, width - sizeof(speed) - sizeof(geted) - sizeof(file), done, total);
//打印进度条
printf("\r%s %s %s %s", file, bar, geted, speed);
fflush(stdout);
if(done == total) // 测试退出条件是否成立
break;
usleep(TIME_INTER) ; // 代表进度条打印间隔时间: 现在是0.7s
}
printf("\n");
}
thread_join.c : 用于回收下载线程的的线程函数实现,注意:主线成另外创建一个线程用来回收下载线程,所以程序运行实际使用的线程数为-t 指定的线程数+2, 主线程用来打印进度条
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#define INTER_VAL 100000 //定义检测线程退出情况的时间间隔,现在是0.1s
extern unsigned short thred_num; // 具体的定义在main文件中的全局变量上
extern void * moloc;
extern void * map;
extern size_t map_len;
//上面四个变量是在main.c 文件中定义的
void swap_tid(pthread_t *x, pthread_t * y)
{
pthread_t tmp;
tmp = *x;
*x = *y;
*y = tmp;
return;
}
void remove_tid(pthread_t * tid, unsigned short len, pthread_t val)
{
for(int i = 0; i < len; i++)
{
if(tid[i] == val)
{
swap_tid(&tid[i], &tid[len-1]);
return ;
}
}
return;
}
void * thread_join(void * args)
{
unsigned short num = thred_num;
pthread_t tid[num]; // 把tid赋值给一个新的数组,以便后来对tid进行改变
for(int i = 0; i < num; i++)
tid[i] = ((pthread_t *)args)[i];
void * retval = NULL;
int ret;
while(1)
{
for(int i = 0; i < num; i++)
{
if( (ret = pthread_tryjoin_np(tid[i], &retval)) == 0 )
{ //线程退出,上面函数返回0,retal 置NULL或者为非NULL的错误退出值
//,此时应该先检测退出值为什么,
//1, 若retval为非NULL值,则整个进程退出,并提示用户断点续传
//2, 若retal为NULL值,把检测的tid数组移除此线程id, 并进行下一轮的检测
if(retval != NULL)
{
//下载中的某个线程出错,退出进程,并提示用户重新下载,利用断点续传下载
printf( "download thread error! please restart this program\n"
"to continue download\n");
free(moloc); //释放主线程申请的资源
munmap(map, map_len);//释放主线程申请的资源
exit(10);
}
else{
// retval为空值,线程正常退出,移除此tid,不再对他进行检测
remove_tid(tid, num, tid[i]);
num --; //检测的线程少一个
}
}
}
if(num == 0) //如果没有检测线程了,则退出检测
break;
usleep(INTER_VAL); // 每搁0.1s检测一次下载线程有没有出错
}
pthread_exit(NULL);
}