linux多线程下载客户端

1、摘要

       最近总需要从网络上下载文件,但是一些链接必须通过分析才能得到,而且,下载的文件必须做一些登记,因此编写了一个能够在其他函数中直接使用的下载模块。

       多线程下载主要需要注意各个线程下载同一文件的不同区域,下载完毕后需要将这些下载的文件块合并为同一个文件。

2、构建多线程下载客户端

1HTTP协议

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段中。然后每个线程读到自己应该读的线程以后即不再读取剩余的内容。将所有的请求获取的数据按序保存到一个文件即可。

2POSIX线程库

       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发送HTTPHEAD请求,获取对应文件的大小信息。首先是封装一个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),程序中采用的办法是如果读取的数据小于buflen4/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响应头的功能。

上面函数多次用到了FREEFREE宏定义如下:

#define         FREE(data)                  free(data);/

                                                            data = NULL

源代码下载

3、作者手记

       作者:悠乐,青娱乐开发工程师。

4、更新日志

5、参考文档

l         Network programming under Unix systems

l         POSIX Threads Programming

l         使用.NET实现断点续传

l         HTTP协议基础

6、相关文档

 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值