Linux下多线程下载兼断点续传实现-原创

原文在我的个人网站上: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 保存的文件名]           如果不指定线程数则使用单线程,不指定文件名,则使用服务器端的名字

软件的一个截屏:

Linux下多线程下载兼断点续传实现-原创_第1张图片

一、开发总体思路如下:
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);
}

你可能感兴趣的:(Linux下多线程下载兼断点续传实现-原创)