libcurl 探索之旅:Multi Interface 中的 curl_multi_fdset 函数究竟是干什么的

一、引言

最近因为工作的原因,我接触到了 libcurl 库。这是一个非常强大的客户端网络库,使用它我们可以完成有关客户端 URL 请求的相关功能。

关于 libcurl 库的编译与使用,我已经在上一篇博客中有所总结,博客地址如下:
libcurl 探索之旅:libcurl 分别在 Unix 环境和 Windows 环境下的编译与使用

在成功编译了 libcurl 库并且成功运行了 libcurl 库源代码中的示例代码之后,我又回到了 libcurl 的官方网站,试图通过官方文档的介绍加上 libcurl 的示例代码进行更为深入的学习。

于是乎,这几天一直都在认真阅读 libcurl 的官方文档。libcurl 的官方文档的阅读当然不是一件简单的事情,其中也遗留了一些我百思不得其解的问题。而这篇博客讨论的主题,就是其中的一个,那就是:

在 libcurl 中的 Multi Interface 的模块中,有个名为 curl_multi_fdset 的函数,它的作用究竟是什么?

或许我直接提出来这个一个概念性的问题,实在有些突兀,不如这篇博客再实际一些,以 libcurl 的官方示例的第一个使用了该函数的例子 10-at-a-time.c 作为研究目标,让我们来仔细研究下,10-at-a-time.c 这份代码,究竟代码逻辑是什么,究竟 curl_multi_fdset 这个函数在这份代码中扮演了怎样的角色。

二、10-at-a-time.c 代码一览

脱离了代码讲述这个问题永远是无力的,这里我左思右想,还是觉得把全部代码粘贴出来比较好,尽管这会导致本篇博客感官上的不美观:

#include <errno.h>
#include <stdlib.h>
#include <string.h>
#ifndef WIN32
#  include <unistd.h>
#endif
#include <curl/multi.h>

static const char *urls[] = {
  "http://www.microsoft.com",
  "http://www.opensource.org",
  "http://www.google.com",
  "http://www.yahoo.com",
  "http://www.ibm.com",
  "http://www.mysql.com",
  "http://www.oracle.com",
  "http://www.ripe.net",
  "http://www.iana.org",
  "http://www.amazon.com",
  "http://www.netcraft.com",
  "http://www.heise.de",
  "http://www.chip.de",
  "http://www.ca.com",
  "http://www.cnet.com",
  "http://www.news.com",
  "http://www.cnn.com",
  "http://www.wikipedia.org",
  "http://www.dell.com",
  "http://www.hp.com",
  "http://www.cert.org",
  "http://www.mit.edu",
  "http://www.nist.gov",
  "http://www.ebay.com",
  "http://www.playstation.com",
  "http://www.uefa.com",
  "http://www.ieee.org",
  "http://www.apple.com",
  "http://www.symantec.com",
  "http://www.zdnet.com",
  "http://www.fujitsu.com",
  "http://www.supermicro.com",
  "http://www.hotmail.com",
  "http://www.ecma.com",
  "http://www.bbc.co.uk",
  "http://news.google.com",
  "http://www.foxnews.com",
  "http://www.msn.com",
  "http://www.wired.com",
  "http://www.sky.com",
  "http://www.usatoday.com",
  "http://www.cbs.com",
  "http://www.nbc.com",
  "http://slashdot.org",
  "http://www.bloglines.com",
  "http://www.techweb.com",
  "http://www.newslink.org",
  "http://www.un.org",
};

#define MAX 10 /* number of simultaneous transfers */
#define CNT sizeof(urls)/sizeof(char *) /* total number of transfers to do */

static size_t cb(char *d, size_t n, size_t l, void *p)
{
  /* take care of the data here, ignored in this example */
  (void)d;
  (void)p;
  return n*l;
}

static void init(CURLM *cm, int i)
{
  CURL *eh = curl_easy_init();

  curl_easy_setopt(eh, CURLOPT_WRITEFUNCTION, cb);
  curl_easy_setopt(eh, CURLOPT_HEADER, 0L);
  curl_easy_setopt(eh, CURLOPT_URL, urls[i]);
  curl_easy_setopt(eh, CURLOPT_PRIVATE, urls[i]);
  curl_easy_setopt(eh, CURLOPT_VERBOSE, 0L);

  curl_multi_add_handle(cm, eh);
}

int main(void)
{
  CURLM *cm;
  CURLMsg *msg;
  long L;
  unsigned int C = 0;
  int M, Q, U = -1;
  fd_set R, W, E;
  struct timeval T;

  curl_global_init(CURL_GLOBAL_ALL);

  cm = curl_multi_init();

  /* we can optionally limit the total amount of connections this multi handle
     uses */
  curl_multi_setopt(cm, CURLMOPT_MAXCONNECTS, (long)MAX);

  for(C = 0; C < MAX; ++C) {
    init(cm, C);
  }

  while(U) {
    curl_multi_perform(cm, &U);

    if(U) {
      FD_ZERO(&R);
      FD_ZERO(&W);
      FD_ZERO(&E);

      if(curl_multi_fdset(cm, &R, &W, &E, &M)) {
        fprintf(stderr, "E: curl_multi_fdset\n");
        return EXIT_FAILURE;
      }

      if(curl_multi_timeout(cm, &L)) {
        fprintf(stderr, "E: curl_multi_timeout\n");
        return EXIT_FAILURE;
      }
      if(L == -1)
        L = 100;

      if(M == -1) {
#ifdef WIN32
        Sleep(L);
#else
        sleep((unsigned int)L / 1000);
#endif
      }
      else {
        T.tv_sec = L/1000;
        T.tv_usec = (L%1000)*1000;

        if(0 > select(M + 1, &R, &W, &E, &T)) {
          fprintf(stderr, "E: select(%i,,,,%li): %i: %s\n",
              M + 1, L, errno, strerror(errno));
          return EXIT_FAILURE;
        }
      }
    }

    while((msg = curl_multi_info_read(cm, &Q))) {
      if(msg->msg == CURLMSG_DONE) {
        char *url;
        CURL *e = msg->easy_handle;
        curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &url);
        fprintf(stderr, "R: %d - %s <%s>\n",
                msg->data.result, curl_easy_strerror(msg->data.result), url);
        curl_multi_remove_handle(cm, e);
        curl_easy_cleanup(e);
      }
      else {
        fprintf(stderr, "E: CURLMsg (%d)\n", msg->msg);
      }
      if(C < CNT) {
        init(cm, C++);
        U++; /* just to prevent it from remaining at 0 if there are more
                URLs to get */
      }
    }
  }

  curl_multi_cleanup(cm);
  curl_global_cleanup();

  return EXIT_SUCCESS;
}

根据官方文档的说法,这份代码的作用,主要是调用了 multi interface 去同时下载了多个文件,而且在下载的过程中,限制了最大同时下载量为 10 个文件。

简单的看看这份代码,你不会觉得很难理解:

1. 初始化 multi interface 的 handle 信息。

一开始,curl_global_init 初始化全局的环境变量,这块没什么好说的,只要认真阅读了官方文档的都清楚。

然后,curl_multi_setopt 设置了 CURLMOPT_MAXCONNECTS 属性,限制了 multi interface 在传输时候的最大同步数量为 MAX,也就是我们自定义的 10。

2. 初始化 10 个传输任务。

通过官方文档我们得知,multi interface 实现多个传输任务同时进行的实质,就是对于多个 easy interface 所使用的句柄进行了类似栈的管理。也就是 multi interface 在另一种意义上来说,确实是多个 easy handle 管理的含义。

这里,代码通过调用了 init 静态函数初始化每个 easy interface 所使用的句柄,配置单次传输的 url 信息、写函数(此处为空)等等。这里的传输类型没有设置,默认为 GET。另外最重要的就是,将 easy interface 所产生的句柄信息加入到了 multi interface 的管理中去,也就是 init 函数中的 curl_multi_add_handle 函数的使用。

至此,我们已经完成了传输任务的配置,下一步当然就是执行传输任务了。

3. 执行传输任务,输出执行结果。

这块是我们这篇博客的讨论重点,代码首先调用了最关键的一行代码:

curl_multi_perform(cm, &U);

使得我们前面设置的传输任务开始执行。返回 U 是开始执行的任务的个数,一般为 10。

然后,中间调用了一系列复杂的函数:

curl_multi_fdset();
curl_multi_timeout();
select();

最后,调用 curl_multi_info_read 去读取传输结果,打印传输信息,清理单次传输的句柄。

最后的最后,递增 C 达到下一个传输任务的初始化以及执行。

这期间,其他逻辑都还是很好理解的,就是中间的三个函数的使用 curl_multi_fdsetcurl_multi_timeout 以及 select 的意义需要认真去思考。

关于这三个函数的意义,我们下一节中认真探讨。

4. 退出清理。

我们需要清理 multi interface 的使用的一些内存空间以及信息。另外,还要清楚一下环境使用的内存空间以及信息。这样才能安全的退出程序。

这里,附图一张该程序的运行截图,以让大家有更加直观的了解:
result

至此,相信我们已经对于 10-at-a-time.c 这份代码已经有了一点认知了,但是我们还遗留了一个重要的问题,那就是那三个函数:

curl_multi_fdset();
curl_multi_timeout();
select();

它们的含义究竟是什么呢?要搞懂这个问题,我们需要去认真阅读两份资料。

三、curl_multi_fdset 释义

我们可以参考下官方文档关于 curl_multi_fdset 的解释文档:
curl_multi_fdset - extracts file descriptors from multi handle

简单的来说,curl_multi_fdset 就是用来从 multi handle 中提取文件描述符的。

以下是 curl_multi_fdset 函数的原型:

#include <curl/curl.h>

CURLMcode curl_multi_fdset(CURLM *multi_handle,
                           fd_set *read_fd_set,
                           fd_set *write_fd_set,
                           fd_set *exc_fd_set,
                           int *max_fd);

其中,各参数的含义如下:

1. multi_handle

这个不需要太多解释,就是指需要提取文件描述符的 multi handle。

2. read_fd_set

这个参数,简而言之,就是返回 fd_set 类型的读文件的文件描述符的集合。

官方文档的解释如下:

If the read_fd_set argument is not a null pointer, it points to an object of type fd_set that on returns specifies the file descriptors to be checked for being ready to read.

也就是说,如果 read_fd_set 的参数不是一个空指针的话,那么它将指向一个 fd_set 类型的参数,这个参数会返回将要被检查的准备好去读的文件的文件描述符信息。

注意这么几个词:
to be checked :将要被检查,为什么还要被检查呢?因为在这个函数中返回的文件描述符所指向的文件,它的状态是未知的。libcurl 只是标记了它是可以用来读的,也就是说,在一次传输任务中,libcurl 告诉我们,有个文件,我们将会去读它,它是我们获取到的信息。但是该文件是否正处于被占用的状态呢?我们可否现在就能去读它呢?这个状态值, curl_multi_fdset 是不知道的,这也就是为什么,我们还需要调用 curl_multi_timeout 去设置超时时间,调用 Linux 的系统函数 select 去监测是否可读的原因。

for being read to read:准备好去读,这个词也就说明了这个参数的含义。curl_multi_fdset 的第二个参数,返回了当前 multi interface 的环境下,所能够读取的文件的文件描述符的信息。

3. write_fd_set

这个参数的含义与上同理,我简单粘贴下官网的定义:

If the write_fd_set argument is not a null pointer, it points to an object of type fd_set that on return specifies the file descriptors to be checked for being ready to write.

也就是,返回 fd_set 类型的写文件的文件描述符的集合。

4. exc_fd_set

这个与上面还是同理:

If the exc_fd_set argument is not a null pointer, it points to an object of type fd_set that on returns specifies the file descriptors to be checked for error condition pending.

也就是,返回 fd_set 类型的错误文件的文件描述符的集合。

5. max_fd

max_fd 返回最大的文件描述符的个数。如果没有找到文件描述符,则返回 -1。

为了详细介绍 max_fd 的作用,我特意把官方文档中的这段话搬了过来:

If no file descriptors are set by libcurl, max_fd will contain -1 when this function returns. Otherwise it will contain the highest descriptor number libcurl set. When libcurl returns -1 in max_fd, it is because libcurl currently does something that isn’t possible for your application to monitor with a socket and unfortunately you can then not know exactly when the current action is completed using select(). You then need to wait a while before you proceed and call curl_multi_perform anyway. How long to wait? Unless curl_multi_timeout gives you a lower number, we suggest 100 milliseconds or so, but you may want to test it out in your own particular conditions to find a suitable value.

这段话比较长,大概就是说,如果 curl_multi_fdset 返回了 -1,有可能是 libcurl 正在做一些事情,你无法使用 socket 去监控到,此时就需要你等待 libcurl 完成。如果你没有通过 curl_multi_fdset 获取到用来操作用的文件操作符,那么后面的 select() 检查操作,以及下一个传输任务的 curl_multi_perform 函数都是无法进行的。此时就必须等待 libcurl 完成当前的任务。

那么,等待多久呢?官方文档给了建议,听从 curl_multi_timeout 的建议,一般就是 100 毫秒。这也是我们的代码逻辑中,当 max_fd 为 -1 的时候,执行的逻辑操作。

综上所述,curl_multi_fdset 函数,就是返回 libcurl 标记的文件描述符的信息。这些文件描述符对应的文件,可以在后面进行读写操作。但是在进行读写操作之前,我们需求实时检查以及确保当前的这些文件描述符对应的文件实实在在的可以进行读写操作,也就是这些文件要么没有被占用要么线程没有被阻塞。这些都不是 curl_multi_fdset 函数能够完成的。

那么问题来了,谁来保证文件真正可以读写了呢(不被占用不被阻塞)?谁来定义一个等待的超时时间呢(不可能一直无限制的等待下去吧)?

完成这个任务的人,就是 Linux 的系统函数 select() 函数啦。

四、select 释义

承接上一节中我们提到的,我们使用 curl_multi_fdset 函数官方文档解释中的一段话来引出 select 函数的使用要点:

When doing select(), you should use curl_multi_timeout to figure out how long to wait for action. Call curl_multi_perform even if no activity has been seen on the fd_sets after the timeout expires as otherwise internal retries and timeouts may not work as you’d think and want.

也就是说,在使用 select() 函数去检查当前的文件描述符是否可以使用的时候,我们需要去定义一个超时时间。

那么,select() 函数究竟是怎么检查文件描述符对应的文件的状态的呢?以及它究竟返回了什么信息呢?

我参考了这篇博客:
linux select函数详解

这篇博客讲的非常好,非常清晰的阐述了 select() 函数的作用,我也希望大家能够花点时间去好好研读下这篇博客。这里,我简单总结下:

1. select() 函数的作用

select() 函数的作用,就是告诉我们,做好了准备的文件描述符的个数,以及做好了什么样的准备(是读,是写,还是异常)。

2. select() 函数的参数

#include <sys/select.h>   

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

select() 函数的参数包括 4 个:

maxfdp1
集合中所有文件描述符的范围,即所有文件描述符的最大值加1。

readset
这里直接截图引用博客里面的信息:

指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读;如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

writeset
同理,只是是写文件描述符。

exceptset
同理,只是是异常文件描述符。

timeout
这个是 select() 函数的超时时间。详细介绍可以参考我上面参考的博客内容。

总而言之,我们使用 curl_multi_fdset 函数来获取到 libcurl 标记的文件描述符,然后使用 select 函数来检查这些文件描述符是否可用。如果 curl_multi_fdset 函数返回了 -1,那么我们需要等待 libcurl 处理完当前的事情,再行获取。同样的,select 函数也需要等待当前的文件处理完成,只有等待 libcurl 处理完了文件之后,我们再来检查文件描述符的状态才有意义。这期间获取 libcurl 的处理完成时间的函数,就是 curl_multi_timeout。

五、回到代码

现在,让我们回到代码再看看代码逻辑,现在已经一目了然了:

if(curl_multi_fdset(cm, &R, &W, &E, &M)) {
        fprintf(stderr, "E: curl_multi_fdset\n");
        return EXIT_FAILURE;
      }

      if(curl_multi_timeout(cm, &L)) {
        fprintf(stderr, "E: curl_multi_timeout\n");
        return EXIT_FAILURE;
      }
      if(L == -1)
        L = 100;

      if(M == -1) {
#ifdef WIN32
        Sleep(L);
#else
        sleep((unsigned int)L / 1000);
#endif
      }
      else {
        T.tv_sec = L/1000;
        T.tv_usec = (L%1000)*1000;

        if(0 > select(M + 1, &F, &W, &E, &T)) {
          fprintf(stderr, "E: select(%i,,,,%li): %i: %s\n",
              M + 1, L, errno, strerror(errno));
          return EXIT_FAILURE;
        }
      }

这段代码:

1. 首先,使用 curl_multi_fdset 获取 libcurl 标记的文件描述符。

2. 然后,使用 curl_multi_timeout 获取 libcurl 当前的等待时间。

3. 判断获取文件描述符是否成功,如果为 -1,则等待第 2 步中获取到的时长。如果获取成功了,则调用 select 函数检查文件描述符的状态。

4. 最后,一旦 select 函数返回值小于 0,也就是发生错误,直接退出。

我们通过 select 函数,就能了解文件的真实状态,完成网络传输下对于文件的操作。不过在这个程序中,好像 select 函数的检查结果并没有产生什么实际的作用 T_T

哈哈,不过这也只是一个示例代码嘛,示例代码的作用,就是尽可能展示库函数的使用方式吧:)

六、总结

本篇博客通过对于 10-at-a-time.c 示例代码的剖析,探讨了 curl_multi_fdset 函数的用法,同时延伸到了 select 函数的探讨。

也许因为我的网络编程经验的欠缺,一些本应该是基础的知识在我的探讨过程都已经成为了自认为的一个问题去思考,不过这也是有意义的嘛,毕竟人生的意义在于思考:)

学习 libcurl 仍在路上

To be Stronger:)

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页