1、摘要
最近总需要从网络上下载文件,但是一些链接必须通过分析才能得到,而且,下载的文件必须做一些登记,因此编写了一个能够在其他函数中直接使用的下载模块。
多线程下载主要需要注意各个线程下载同一文件的不同区域,下载完毕后需要将这些下载的文件块合并为同一个文件。
2、构建多线程下载客户端
1、HTTP协议
HTTP协议是一种基于tcp的简单协议,分为请求和回复两种。请求协议是由客户机(浏览器)向服务器(WEB SERVER)提交请求时发送报文的协议。回复协议是由服务器(web server),向客户机(浏览器)回复报文时的协议。请求和回复协议都由头和体组成。头和体之间以一行空行为分隔。注意空行统一使用/r/n,共两个字节。
以下是一个请求报文与相应的回复报文的例子:
GET /image/index_r4_c1.jpg HTTP/1.1
Accept: */*
Referer: http://192.168.3.120:8080
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)
Host: 192.168.3.120:8080
Connection: Keep-Alive
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Tue, 24 Jun 2003 05:39:40 GMT
Content-Type: image/jpeg
Accept-Ranges: bytes
Last-Modified: Thu, 23 May 2002 03:05:40 GMT
ETag: "bec48eb 862c 21:934"
Content-Length: 2827
需要注意HTTP请求支持Range,正是利用该特性,我们才能实现多线程下载(断点续传等也是利用该特性)。在HTTP协议中,可以在请求报文头中加入Range段,来表示客户机希望从何处继续下载。比如说从第1024字节开始下载,请求报文如下:
GET /image/index_r4_c1.jpg HTTP/1.1
Accept: */*
Referer: http://192.168.3.120:8080
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)
Host: 192.168.3.120:8080
Range:bytes=1024-
Connection: Keep-Alive
因此,如果需要使用多线程下载,我们启动多个线程,每个线程发送一个GET请求,请求同一文件,但是每个请求的开始地址不同,写入Range段中。然后每个线程读到自己应该读的线程以后即不再读取剩余的内容。将所有的请求获取的数据按序保存到一个文件即可。
2、POSIX线程库
POSIX线程库的详细介绍在这里,也可以从这里下载一个pdf版的介绍。本文简单介绍一些程序中用到的函数。
l 启动线程
int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void
*(*start_routine) (void *), void *arg) ;
thread为线程ID指针,attr为线程属性,start_routine则为线程启动时执行的函数,注意该函数应该为(void *)func(void *data),其他类型的函数需做强制转换,arg则为传递给func函数的参数。
l pthread_join方法
int pthread_join (pthread_t thread, void **value_ptr);
该方法将阻塞调用线程,直到线程thread执行结束。
3、实现客户端的几个函数
为实现客户端功能,首先需要从要下载文件的URL中分析出服务器域名、端口以及文件在服务器上的路径等信息。接着的步骤如下:
1. 解析域名,获取服务器IP地址
2. 与服务器建立连接,发送HEAD请求,确认待下载文件存在,获取待下载文件大小
3. 依据待下载文件大小将文件分块,保存到分块链表中。依据总块数以及用户指定的线程数确定实际应该启动的线程数目。
4. 启动线程下载文件。每个线程从分块链表中取出链表中第一个块,下载文件。知道链表为空。
5. 关闭连接。程序执行完毕。
main函数
main函数主要用于解析命令行参数。
while ((tmp = getopt(argc, argv, "u:f:n:")) != -1)
{
switch (tmp)
{
case 'u':
if ((url = (char *)malloc(strlen(optarg) + 1)) != NULL)
strcpy(url, optarg);
break;
case 'f':
if ((localfile = (char *)malloc(strlen(optarg) + 1)) != NULL)
strcpy(localfile, optarg);
break;
case 'n':
thdCount = atoi(optarg);
break;
case ':': //no break
case '?': //no break
default:
printf(cmdLine, argv[0]);
break;
}
}
在main中主要是分析用户输入的命令,如文件url,本地文件保存地址,允许启动的线程数等。接着,直接将url传递给函数mulDownFile,由其负责所有的下载操作。这样以便以后将该功能继承到其他程序中。
mulDownFile函数
考虑到下载一个文件,首先需要获取该文件的位置,包括服务器地址,文件在服务器上的路径等。
mulDownFile 函数定义如下:
int mulDownFile(const char *url, const char *localfile, int thdCount)
其中url中可以提取需要的域名和路径信息,由域名可以获取服务器的地址,同时url隐藏了服务器的端口信息,如果url中使用:指明端口,则服务器端口由此获取,否则,端口默认为80.
if (!strncasecmp(url, "http://", strlen("http://")))
{
url += strlen("http://");
}
if (tmp = strchr(url, ':')) //get host domain and port from url
{
strncpy(szHost, url, tmp - url);
strncpy(szPort, tmp + 1, strchr(url, '/') - tmp - 1);
port = atoi(szPort);
}
if (tmp = strchr(url, '/')) //get host domain and remote file path
{
if (strlen(szHost) == 0)
strncpy(szHost, url, tmp - url);
strcpy(szRemoteFile, tmp);
if (localfile == NULL) //get local file path
{
tmp = strrchr(url, '/');
strcpy(szLocalFile, tmp + 1);
}
else
{
strcpy(szLocalFile, localfile);
}
}
else
{
printf("url error/n");
return -1;
}
另外需要注意的是,如果该函数接收了NULL指针指向本地文件名,那么,将从url中分析文件名称。无论是用户指定文件名,还是自动生成的文件名,都存在着与现有文件重名的风险,因此,程序需要判断该情况。本程序中采用的办法是,如果文件重名,则在文件名后添加_x后缀。
int fd;
while ((fd = open(szLocalFile, O_WRONLY | O_CREAT | O_EXCL, S_IRWXU | S_IRGRP
| S_IROTH)) == -1)
{
if (errno == EEXIST)
{
if (strlen(szLocalFile) + 2 >= sizeof(szLocalFile))
{
printf("file exist, choose another/n");
return -1;
}
strcat(szLocalFile, "_x"); //get unique file name
}
else
{
printf("can not save file: %s", strerror(errno));
return -1;
}
}
close(fd);
接下来需要解析域名,保存。
host = gethostbyname(szHost))
余下的工作则是由host来获取sockaddr结构,建立连接。建立连接,获取了对应的socket des之后,首先发送HEAD请求,获取文件长度。该功能由getLength函数单独完成。
获取文件长度以后,我们需要确定将文件分块下载。程序中将文件分块大小定义为 1M 字节:
#define BLOCK_SIZE (1024 * 1024)
每个线程每次下载一个文件块,但是并不是有多少个文件块就启动多少个线程。启动线程的数量取以下三者的最小值:用户输入的线程数目,程序分块数目,程序定义的最大线程数,其中后者为:
#define MAX_THREAD_COUNT 10
当实际启动的线程小于文件总块数时怎么办?程序中将所有的分块信息链接成为一个链表,每个线程从链表中取分块信息,下载对应分块,直到所有的分块下载完毕。对每个分块,使用如下结构保存:
typedef struct iblock
{
int index;
int blockSize;
struct iblock* next;
} stBlock, *IBLOCK ;
此时可以启动线程来下载文件了。线程执行download函数。由于download函数需要从服务器下载文件,由此,需要参数服务器域名,远程文件路径,本地文件路径,服务器地址sockaddr。而posix线程库要求线程启动的函数的参数为void*类型,因此,我们可以将上述信息封装为一个struct,或者直接使用指针的指针:
void **data = (void **)malloc(sizeof(void *) * 4);
data[0] = (void *)malloc(strlen(szHost) + 1);
strcpy((char *)data[0], szHost);
data[1] = (void *)malloc(strlen(szRemoteFile) + 1);
strcpy((char *)data[1], szRemoteFile);
data[2] = (void *)malloc(strlen(szLocalFile) + 1);
strcpy((char *)data[2], szLocalFile);
data[3] = &servaddr;
信息封装以后则可以直接启动线程来下载文件了。需要注意的是主线程启动了所有的下载线程以后,不能立即退出,而是等待下载线程全部执行完毕了才能退出:
for (i = 0; i < thdCount; i++)
{
if (pthread_create(&threads[i], NULL, (void *)download, (void *)data))
{
printf("can not create thread %d/n", i);
}
}
for (i = 0; i < thdCount; i++)
{
pthread_join(threads[i], NULL);
}
getLength函数
函数原型如下:
static long getLength(int sfd, const char *domain, const char *remotePath)
该函数向已经连接的socket desc发送HTTP的HEAD请求,获取对应文件的大小信息。首先是封装一个HEAD请求的字符串:
sprintf(szRequest, "HEAD %s HTTP/1.1 /r/nAccept: */*/r/nAccept-Language: zh-cn /r/nAccept-Encoding: gzip, deflate /r/nUser-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) /r/nHost:%s/r/nConnection: Keep-Alive/r/n/r/n", remotePath, domain);
接着调用getHead方法获取HTTP服务器的响应信息:
response = getHead(sfd, szRequest);
获取响应信息以后,则仅仅需要从response字符串中分析出长度值。该长度值由Content-Length指出。
getHead函数
该函数功能很简单,发送一个请求,接收所有响应数据,使用基本的同步阻塞模式:
if ((rc = write(sfd, request, strlen(request))) == strlen(request))
{
result = (char *)malloc(resultlen);
memset(result, 0, resultlen);
buf = (char *)malloc(buflen);
memset(buf, 0, buflen);
while ((rc = read(sfd, buf, buflen - 1))) //-->1
{
if ((resultlen - strlen(result)) < rc + 1)
{
resultlen *= 2;
result = (char *)realloc(result, resultlen);
}
strncat(result, buf, rc);
// if rc less than 4/5 of buflen, the read func should return
if (rc < buflen * 4 / 5) //-->2
{
break;
}
}
FREE(buf);
}
问题:我发现当所有数据被读出以后,有些时候read并不返回,而是等待状态(代码行1),这样,在同步阻塞模式下我们需要自己知道什么时候结束读取动作(代码行2),程序中采用的办法是如果读取的数据小于buflen的4/5即返回,用以标记数据被读取完毕。此方法并不可取,但如何解决该问题呢?
download函数
函数原型如下:
static void download(void *data);
由于该函数作为线程执行的函数,因此其参数被定义为void *类型,该函数执行时,首先将参数从void *中提取出来:
char **dats = (char **)data;
char *host = dats[0];
char *remotePath = dats[1];
char *localFile = dats[2];
struct sockaddr_in * pServAddr = (struct sockaddr_in *)dats[3];
接下来的执行流程为从需要下载的文件块链表中取出链表首元素(此过程需要使用互斥量保证临界资源),下载该元素对应的块文件,写入到物理文件中。需要注意的是此过程是一个无限循环,即每个循环下载一个块,当链表为空时才退出循环。
while (1)
{
pthread_mutex_lock(&m_mutex);
if (blkfirst == NULL)
{
pthread_mutex_unlock(&m_mutex);
break;
}
current = blkfirst;
blkfirst = blkfirst->next;
index = current->index;
blockSize = current->blockSize;
free(current);
pthread_mutex_unlock(&m_mutex);
printf("downloading No. [%d] block, size of [%d] /n", index, blockSize);
offset = index * BLOCK_SIZE;
if ((fd = open(localFile, O_WRONLY)) == -1)
{
printf("can not open file %s to write: %s/n", localFile, strerror(errno));
return;
}
if ((lseek(fd, offset, SEEK_SET)) == -1)
{
printf("can not operate file %s to write: %s/n", localFile, strerror(errno));
return;
}
if ((sfd = getSocket(*pServAddr)) == -1)
{
return;
}
sprintf(szRequest, "GET %s HTTP/1.1 /r/nAccept: */*/r/nAccept-Language: zh-cn /r/nAccept-Encoding: gzip, deflate /r/nUser-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) /r/nHost:%s/r/nRange:bytes=%ld-/r/nConnection: Keep-Alive/r/n/r/n", remotePath, host, offset);
getData(sfd, fd, szRequest, blockSize);
close(fd);
close(sfd);
printf("No. [%d] block, size of [%d] downloaded/n", index, blockSize);
}
函数getSocket负责与服务器建立socket连接,返回socket desc。实际的下载工作由getData函数完成,该函数从sfd中读取blockSize的数据,写入到fd中。需要注意的是每次sfd都会返回HTTP响应头,因此,程序必须过滤这些信息。
if ((rc = write(sfd, request, strlen(request))) == strlen(request))
{
buf = (char *)malloc(buflen);
memset(buf, 0, buflen);
while (length > 0)
{
char *pbuf = buf;
rc = read(sfd, buf, buflen - 1);
if (!escaped)
{
int len;
char *tmp;
char *lenptr;
char *lenstart = "Content-Length: ";
char *lenend = "/r/n";
escaped = 1;
buf[rc] = 0;
lenptr = strstr(buf, lenstart);
lenptr += strlen(lenstart);
tmp = strstr(lenptr, lenend);
pbuf = strstr(pbuf, "/r/n/r/n");
pbuf += 4;
*tmp = 0;
len = atol(lenptr);
length = min(length, len);
rc -= (pbuf - buf);
}
tmplen = min(rc, length);
write(fd, pbuf, tmplen);
length -= tmplen;
//printf("%d ==> %d/%d/n",fd, tmplen, length);
}
FREE(buf);
}
注意到两点,一是此时我们不再read是依据length是否大于0,而不是之前getHead采用的方法,因为此时我们知道需要读多少字节的数据;而是黑体部分是完成过滤HTTP响应头的功能。
上面函数多次用到了FREE,FREE宏定义如下:
#define FREE(data) free(data);/
data = NULL
源代码下载
3、作者手记
作者:悠乐,青娱乐开发工程师。
4、更新日志
5、参考文档
l Network programming under Unix systems
l HTTP协议基础