c-ares dns 异步请求库简明教程

2017/05/01 c c++

简介

c-ares是一个C语言实现的异步请求DNS的实现。很多知名 软件(curl、seastar、gevent、Nodejs等等)都使用了该软件。但是该软件相关的中文资料很少,就 此以几个实例来简要说明该库的使用教程。

使用c-ares时通常只需要引用ares.h头文件即可,该库的相关头文件都包含在内。

以下先介绍几个该库初始化相关的函数

init

ares_library_init(ARES_LIB_INIT_ALL)

在实例初始化时,应该先调用该函数对该库相关内部模块(其实该初始化函数只在win32平台上有实际动作)。

ares_library_cleanup()

在实例销毁时调用该函数进行反初始化。与ares_library_init(ARES_LIB_INIT_ALL)向对应。

ares_library_init_mem

初始化时指定该库内部内存分配的malloc/free/realloc函数(如不指定则默认使用glibc提供的)。 然后调用ares_library_init

ares_channel

c-aresares_channel作为整个异步请求的上下文(context),与请求相关的函数用ares_channel 贯穿起来调用。因此发起一个请求时,应该先初始化ares_channel

ares_channel需要通过ares_init_options或者ares_init进行初始化,区别是ares_init使用默认 参数,ares_init_options支持自定义参数,自定义参数为struct ares_optionsint optmaskoptmask 用于决定使用struct ares_options中的那些字段。optmask的定义对照struct ares_options中的字段对照 一看就能明白,其值有(也可以参看文档:

#define ARES_OPT_FLAGS          (1 << 0)
#define ARES_OPT_TIMEOUT        (1 << 1)
#define ARES_OPT_TRIES          (1 << 2)
#define ARES_OPT_NDOTS          (1 << 3)
#define ARES_OPT_UDP_PORT       (1 << 4)
#define ARES_OPT_TCP_PORT       (1 << 5)
#define ARES_OPT_SERVERS        (1 << 6)
#define ARES_OPT_DOMAINS        (1 << 7)
#define ARES_OPT_LOOKUPS        (1 << 8)
#define ARES_OPT_SOCK_STATE_CB  (1 << 9)
#define ARES_OPT_SORTLIST       (1 << 10)
#define ARES_OPT_SOCK_SNDBUF    (1 << 11)
#define ARES_OPT_SOCK_RCVBUF    (1 << 12)
#define ARES_OPT_TIMEOUTMS      (1 << 13)
#define ARES_OPT_ROTATE         (1 << 14)
#define ARES_OPT_EDNSPSZ        (1 << 15)
#define ARES_OPT_NOROTATE       (1 << 16)
struct ares_options {
  int flags;   // 参见下段描述
  int timeout; /* in seconds or milliseconds, depending on options */ // 请求超时的时间、默认是5s
  int tries;   // 默认跟每个nameserver重试请求的次数
  int ndots;
  unsigned short udp_port; // nameserver udp port
  unsigned short tcp_port; // nameserver tcp port
  int socket_send_buffer_size;
  int socket_receive_buffer_size;
  struct in_addr *servers; // nameserver list
  int nservers;            // servers count
  char **domains;// 强制进行dns查询的域,不按照hosts、resolve.conf内指定
  int ndomains;  // domains count
  char *lookups; // 默认查找的范围 'b' 代表走DNS查询、'f'代表查询`hosts`文件,默认可以填'fb'
  ares_sock_state_cb sock_state_cb;  // socket 状态变更时的回调
  void *sock_state_cb_data;          // 回调的私有数据
  struct apattern *sortlist;
  int nsort;
  int ednspsz;
};

ares_options中的flags的有效字段:

#define ARES_FLAG_USEVC         (1 << 0) // 总是使用tcp进行dns查询
#define ARES_FLAG_PRIMARY       (1 << 1)
#define ARES_FLAG_IGNTC         (1 << 2)
#define ARES_FLAG_NORECURSE     (1 << 3)
#define ARES_FLAG_STAYOPEN      (1 << 4)
#define ARES_FLAG_NOSEARCH      (1 << 5)
#define ARES_FLAG_NOALIASES     (1 << 6)
#define ARES_FLAG_NOCHECKRESP   (1 << 7)
#define ARES_FLAG_EDNS          (1 << 8)

ares_channel创建并初始化后,可以对其再进行一些设置。常见的设置函数有:

// 设置请求时的本地ip
void ares_set_local_ip4(ares_channel channel, unsigned int local_ip);
void ares_set_local_ip6(ares_channel channel, const unsigned char* local_ip6);

// 设置请求时跟哪个网卡绑定在一起,当有多个网卡都对外可达时,可以用此函数选取绑定一个网卡
void ares_set_local_dev(ares_channel channel, const char* local_dev_name);

// 注册一个回调,当c-ares socket被创建后,配置完成一些c-ares默认配置后,会触发该回调。当需要再自定义
// 一些设置时,可以在此实现。
void ares_set_socket_configure_callback(ares_channel channel, ares_sock_config_callback callback, void *user_data);

// 注册一个回调,当c-ares socket调用connect后,会触发该回调。如果需要在创建连接后做一些自定义设置时,
// 可以在此实现,例如tcp的keepalive等
void ares_set_socket_callback(ares_channel channel, ares_sock_create_callback callback, void *user_data);

// 当用户需要hook/hack 网卡IO时,用户可以注册socket相关的回调。回调中的几个函数分别对应
// socket、close、connect、recvfrom、sendv几个系统回调。当用户注册后,c-ares的跟网络IO有关
// 系的操作会调用该回调中的方法,而不会调用默认的系统调用了。
struct ares_socket_functions {
   ares_socket_t(*asocket)(int, int, int, void *);
   int(*aclose)(ares_socket_t, void *);
   int(*aconnect)(ares_socket_t, const struct sockaddr *, socklen_t, void *);
   ssize_t(*arecvfrom)(ares_socket_t, void *, size_t, int, struct sockaddr *, socklen_t *, void *);
   ssize_t(*asendv)(ares_socket_t, const struct iovec *, int, void *);
};

void ares_set_socket_functions(ares_channel channel, const struct ares_socket_functions *funcs, void *user_data);

process

c-ares整个请求的异步逻辑都封装在该库内部,但是内部并不驱动这些逻辑运转,c-ares将逻辑驱动暴露给用户调用, 让用户驱动整个逻辑的运转,方便将其嵌入各种事件框架中去。

相关的函数有:

int ares_fds(ares_channel channel, fd_set *read_fds, fd_set *write_fds);

int ares_getsock(ares_channel channel, ares_socket_t *socks, int numsocks);

struct timeval *ares_timeout(ares_channel channel, struct timeval *maxtv, struct timeval *tv);

void ares_process(ares_channel channel, fd_set *read_fds, fd_set *write_fds);

void ares_process_fd(ares_channel channel, ares_socket_t read_fd, ares_socket_t write_fd);

整个请求的处理过程将在例子中给出。

error

当函数返回错误时,可以调用const char *ares_strerror(int code)获取错误码对应的错误原因。

util

c-ares还提供一些util函数,通常是一些封装,提供跨平台的能力。这些函数名同linux中glibc提供的api 命名相似,功能也相似

const char *ares_inet_ntop(int af, const void *src, char *dst, ares_socklen_t size);
int ares_inet_pton(int af, const char *src, void *dst);

example

select

下面这个例子来自于stackoverflow.com, 该例子通过ip地址反查nameserver的域名:

#include <sys/select.h>
#include <ares.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <time.h>
#include <iostream>

void dns_callback(void* arg, int status, int timeouts, struct hostent* host) {
  if (status == ARES_SUCCESS)
    std::cout << host->h_name << std::endl;
  else
    std::cout << "lookup failed: " << ares_strerror(status) << std::endl;
}

void main_loop(ares_channel& channel) {
  int nfds, count;
  fd_set readers, writers;
  timeval tv, *tvp;
  while (1) {
    FD_ZERO(&readers);
    FD_ZERO(&writers);
    // 获取c-ares内部需要处理的fd
    nfds = ares_fds(channel, &readers, &writers);
    if (nfds == 0) break;
    // 获取超时时间
    tvp = ares_timeout(channel, NULL, &tv);
    // 调用select来等待fd达到条件
    count = select(nfds, &readers, &writers, NULL, tvp);
    // 将fd的新状态交于c-ares内部处理
    ares_process(channel, &readers, &writers);
  }
}

int main(int argc, char** argv) {
  struct in_addr ip;
  int res;
  if (argc < 2) {
    std::cout << "usage: " << argv[0] << " ip.address\n";
    return 1;
  }
  // 读取输入的ip地址
  inet_aton(argv[1], &ip);
  ares_library_init(ARES_LIB_INIT_ALL);
  // 初始化channel
  ares_channel channel;
  if ((res = ares_init(&channel)) != ARES_SUCCESS) {
    std::cout << "ares feiled: " << res << '\n';
    return 1;
  }
  // 调用查询api,当main_loop中驱动时间运转时,查询有结果时会触发回调dns_callback
  ares_gethostbyaddr(channel, &ip, sizeof ip, AF_INET, dns_callback, NULL);
  main_loop(channel);
  ares_library_cleanup();
  return 0;
}
libevent

下面的例子展示c-ares如何跟libevent的事件驱动框架相结合:

// g++ example.cc -lcares -levent --std=c++11
#include <ares.h>
#include <event.h>
#include <stdio.h>
#include <sys/socket.h>
#include <iostream>
#include <string>
#include <unordered_map>

std::string AddressToString(void *vaddr, int len) {
  auto addr = reinterpret_cast<unsigned char *>(vaddr);
  std::string addv;
  if (len == 4) {
    char buff[4 * 4 + 3 + 1];
    sprintf(buff, "%u.%u.%u.%u", addr[0], addr[1], addr[2], addr[3]);
    return addv.assign(buff);
  } else if (len == 16) {
    for (int ii = 0; ii < 16; ii += 2) {
      if (ii > 0) addv.append(":");
      char buff[4 + 1];
      sprintf(buff, "%02x%02x", addr[ii], addr[ii + 1]);
      addv.append(buff);
    }
  }
  return addv;
}

struct context {
  struct event_base *base;
  struct event timeout_evt;
  ares_channel channel;
  ares_options ares_opts;
  std::unordered_map<int, struct event> events;
};

int main(int argc, char **argv) {
  context ctx = {};
  char lookups[] = "fb";
  int res;
  struct timeval tv, *tvp;

  if (argc < 2) {
    std::cout << "usage: " << argv[0] << "hostname" << std::endl;
    return -1;
  }

  ares_library_init(ARES_LIB_INIT_ALL);

  ctx.base = event_init();
  ctx.ares_opts.lookups = lookups;
  ctx.ares_opts.timeout = 3000;
  ctx.ares_opts.tries = 1;
  ctx.ares_opts.sock_state_cb_data = &ctx;
  ctx.ares_opts.sock_state_cb = [](void *arg, int fd, int readable,
                                   int writable) {
    auto ctx = reinterpret_cast<context *>(arg);
    short events = 0;
    auto &event = ctx->events[fd];
    if (readable) events |= EV_READ;
    if (writable) events |= EV_WRITE;

    if (events == 0) {
      event_del(&event);
      ctx->events.erase(fd);
      return;
    }

    if (event_assign(&event, ctx->base, fd, events,
                     [](int fd, short events, void *arg) {
                       int w = ARES_SOCKET_BAD, r = ARES_SOCKET_BAD;
                       auto ctx = reinterpret_cast<context *>(arg);
                       if (events & EV_READ) r = fd;
                       if (events & EV_WRITE) w = fd;
                       ares_process_fd(ctx->channel, r, w);
                     },
                     ctx) != 0) {
      std::cout << "event_assign failed" << std::endl;
      return;
    }
    if (event_add(&event, nullptr) != 0) {
      std::cout << "event_add failed" << std::endl;
    }
  };

  if ((res = ares_init_options(&ctx.channel, &ctx.ares_opts,
                               ARES_OPT_TIMEOUTMS | ARES_OPT_TRIES |
                                   ARES_OPT_SOCK_STATE_CB |
                                   ARES_OPT_LOOKUPS)) != ARES_SUCCESS) {
    std::cout << "ares feiled: " << ares_strerror(res) << std::endl;
    return res;
  }

  event_assign(&ctx.timeout_evt, ctx.base, -1, EV_TIMEOUT,
               [](int fd, short events, void *arg) {
                 (void)fd;
                 (void)events;
                 auto ctx = reinterpret_cast<context *>(arg);
                 ares_process_fd(ctx->channel, ARES_SOCKET_BAD,
                                 ARES_SOCKET_BAD);
               },
               &ctx);

  ares_gethostbyname(
      ctx.channel, argv[1], AF_INET,
      [](void *data, int status, int timeouts, struct hostent *hostent) {
        auto ctx = reinterpret_cast<context *>(data);

        if (timeouts > 0) {
          std::cout << "loopkup timeout" << std::endl;
          return;
        } else {
          event_del(&ctx->timeout_evt);
        }

        if (status != ARES_SUCCESS) {
          std::cout << "lookup failed: " << ares_strerror(status) << std::endl;
          return;
        }

        if (hostent->h_addr_list) {
          char **paddr = hostent->h_addr_list;
          while (*paddr != nullptr) {
            std::cout << "ip: " << AddressToString(*paddr, hostent->h_length)
                      << std::endl;
            paddr++;
          }
        }
      },
      &ctx);

  tvp = ares_timeout(ctx.channel, NULL, &tv);
  event_add(&ctx.timeout_evt, tvp);
  if ((res = event_base_loop(ctx.base, 0)) < 0) {
    std::cout << "event base loop failed: " << res << std::endl;
    return res;
  }

  ares_destroy(ctx.channel);
  ares_library_cleanup();
  return 0;
}

Search

    Table of Contents