源码编译

  • 使用 AutoTools 编译(仅限 Git 仓库源码,非 Release 版本)。
  • 使用 Cmake 编译。

AutoTools 编译

对于 Unix 平台,可以使用 AutoTools 工具生成 c-ares 编译脚本。

Autotools是一个用于自动化软件构建的工具集,包括 Autoconf、Automake 和 Libtool。

  • autoconfig 用于将 configure.ac中 的宏展开,生成 configure 脚本。
  • automake 用于将 Makefile.am 中定义的结构生成 Makefile.in。
1
2
3
4
# 通过 HomeBrew 安装 AutoTools
$ brew install Autotools
# 或者
$ brew install autoconf automake libtool

编译步骤

  1. 执行 ./buildconf (Unix) 或者 buildconf.bat (Windows) 生成编译脚本文件。
  2. 执行 ./configure,生成 makefile。
  3. 执行 make 命令,编译源码。
  4. 执行 make install
1
2
3
4
5
# iOS 
./configure --host=arm-apple-darwin --disable-static --enable-shared --with-sysroot=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain

# Android
./configure --host=aarch64-linux-android --disable-static --enable-shared --with-sysroot=${NDK_PATH}/toolchains/llvm/prebuilt/darwin-x86_64/sysroot

Cmake 编译

c-ares 支持 cmake v3 以上编译系统构建。支持 Windows, Linux, FreeBSD, MacOS, AIX and Solaris 等常见平台。

编译步骤

  1. 执行 cmake,生成对应平台的编译系统 。
  2. 执行 make 命令,编译源码。
  3. 执行 make install
1
2
3
4
5
6
# iOS (需要额外下载 ios.toolchain.cmake)
cmake .. -G Xcode -DCMAKE_TOOLCHAIN_FILE=/xxx/ios.toolchain.cmake -DPLATFORM=OS64 -DENABLE_STRICT_TRY_COMPILE=ON

# Android
# -DCMAKE_ANDROID_STL_TYPE=gnustl_static -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28
cmake -DCMAKE_SYSTEM_NAME=android -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a -DCMAKE_SYSTEM_VERSION=28 -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake

c-ares 可选配置:

  • CARES_STATIC,编译静态库,默认 OFF。
  • CARES_SHARED,编译动态库,默认 ON。
  • CARES_INSTALL,创建安装目标,默认 ON。
  • CARES_STATIC_PIC,编译 PIC (position independent)静态库,默认 OFF。
  • CARES_BUILD_TESTS,编译运行测试代码,默认 OFF。
  • CARES_BUILD_TOOLS,编译命令行工具代码,默认 ON。
  • CARES_SYMBOL_HIDING, 在动态库中隐藏私有符号”Hide,默认OFF。

IDE 手动编译

编译步骤

  1. 生成 ares_build.h 和 ares_config.h。(需通过 cmake 或者 configure)
  2. 将 include 和 src/lib 下的源码和头文件加入到 IDE 工程中,并添加预编译宏 “HAVE_CONFIG_H=1”。(若编译动态库,还需要加上 CARES_BUILDING_LIBRARY 宏以确保接口导出)

异步解析

  1. c-ares 初始化。
  2. [可选] Android等平台额外初始化。
  3. 创建 c-ares 通道,并设置相关可选参数。
  4. 添加域名解析任务。
  5. 驱动域名解析执行(消息收发、回调处理)。
  6. 销毁 c-ares 通道。
  7. 清理 c-ares。
  • win 平台,在初始化 c-ares 之前,需要调用 WSAStartup,在清理 c-ares 之后,需要调用 WSACleanup,来启用 win 网络。
  • Android 平台,在 JNI_OnLoad 时,需要缓存 jvm 用于 JNI 调用,并通过 ares_library_init_jvm 设置给 c-ares。 然后在 onCreate 或其他合适时机,将 ConnectivityManager 传递到 Native 层并缓存(也可以传递 application context,然后在 native 层通过 JNI 获得 ConnectivityManager 实例)。当 c-ares 初始化完后,需要调用 ares_library_init_android 进行 Android 平台的额外设置,将 ConnectivityManager 设置给 c-ares 用于获取默认 DNS Servers。因为清理 c-ares 时,内部会自动释放 ConnectivityManager 引用,故每次c-ares 初始化后,都要重新设置 ConnectivityManager。(Android 8 或者 target 26 以上必须调用,否则不设置 DNS servers 的情况下,无法正常解析)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
void my_dns_callback(void* arg, int status, int timeouts, struct ares_addrinfo* ai) {

printf("getaddrinfo callback, arg:%lld, status: %d, timeouts:%d\n", (long long)arg, status, timeouts);

if (status == ARES_SUCCESS) {
struct ares_addrinfo_node* iter = ai->nodes;
while (iter) {
char addr[INET6_ADDRSTRLEN];
if (iter->ai_family == AF_INET) {
struct sockaddr_in* sa = (struct sockaddr_in*)iter->ai_addr;
ares_inet_ntop(AF_INET, &(sa->sin_addr), addr, INET_ADDRSTRLEN);
}
else if (iter->ai_family == AF_INET6) {
struct sockaddr_in6* sa = (struct sockaddr_in6*)iter->ai_addr;
ares_inet_ntop(AF_INET6, &(sa->sin6_addr), addr, INET6_ADDRSTRLEN);
}
printf("Resolved IP address: %s\n", addr);
iter = iter->ai_next;
}
ares_freeaddrinfo(ai);
}
else {
printf("DNS resolution failed: %d, %s\n", status, ares_strerror(status));
}
}

void my_dns_test()
{
#ifdef WIN32
WSADATA stWsa;
WORD wVer = MAKEWORD(2, 2);

WSAStartup(wVer, &stWsa);
#endif

// 初始化 c-ares
int status = ares_library_init(ARES_LIB_INIT_ALL);
if (status != ARES_SUCCESS) {
fprintf(stderr, "Failed to initialize c-ares: %s\n", ares_strerror(status));
return ;
}

// 创建 DNS 通道
ares_options opt;
opt.flags = ARES_FLAG_USEVC; // 使用 TCP,mask:ARES_OPT_FLAGS
opt.timeout = 10; // 设置超时时间,mask:ARES_OPT_TIMEOUT

ares_channel channel;
status = ares_init_options(&channel, &opt, ARES_OPT_TIMEOUT);
if (status != ARES_SUCCESS) {
fprintf(stderr, "Failed to create DNS channel: %s\n", ares_strerror(status));
ares_library_cleanup();
return ;
}

// 设置 DNS 服务器
ares_set_servers_csv(channel, "114.114.114.114,8.8.8.8"); // 多个地址中间用 , 隔开

// 添加域名解析任务
long long arg = 0;
struct ares_addrinfo_hints hint;
memset(&hint, 0x00, sizeof(hint));
hint.ai_family = AF_UNSPEC;
ares_getaddrinfo(channel, "www.qq.com", NULL, &hint, my_dns_callback, (void*)arg++);
ares_getaddrinfo(channel, "google.com", NULL, &hint, my_dns_callback, (void*)arg++);

// 驱动 DNS 解析
my_ares_dns_loop(channel);

// 清理 DNS 通道
ares_destroy(channel);

// 清理 c-ares
ares_library_cleanup();

#ifdef WIN32
WSACleanup();
#endif
}

可以使用 select、poll、epoll、kqueue等实现不同平台的驱动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244

#if defined(__APPLE__)
void main_loop_kqueue(ares_channel& channel)
{
while (1) {

// 创建 kqueue
int kq = kqueue();
if (kq == -1) {
printf("kqueue error:%d", errno);
break;
}

ares_socket_t socks[ARES_GETSOCK_MAXNUM];
int bitmask = ares_getsock(channel, socks, ARES_GETSOCK_MAXNUM);

struct kevent evlist[10];

int socknum = 0;
for (int i = 0; i < ARES_GETSOCK_MAXNUM; i++) {
bool empty = true;

if (ARES_GETSOCK_READABLE(bitmask, i)) {
struct kevent readev;
EV_SET(&readev, socks[i], EVFILT_READ, EV_ADD, 0, 0, NULL);
if (kevent(kq, &readev, 1, NULL, 0, NULL) == -1) {
continue;
}
empty = false;
}
if (ARES_GETSOCK_WRITABLE(bitmask, i)) {
struct kevent writeev;
EV_SET(&writeev, socks[i], EVFILT_WRITE, EV_ADD, 0, 0, NULL);
if (kevent(kq, &writeev, 1, NULL, 0, NULL) == -1) {
continue;
}
empty = false;
}

if (empty) {
break;
}
else {
socknum++;
}
}

if (socknum > 0) {

timeval tv, * tvp;
tvp = ares_timeout(channel, NULL, &tv);

struct timespec timeout;
timeout.tv_sec = tvp->tv_sec;
timeout.tv_nsec = tvp->tv_usec * 1000;

int nev = kevent(kq, NULL, 0, evlist, 10, &timeout);
printf("kevent ret:%d!\n", nev);
if (nev > 0) {
for (int i = 0; i < nev; i++) {
ares_process_fd(channel,
evlist[i].filter == EVFILT_READ ? evlist[i].ident : ARES_SOCKET_BAD,
evlist[i].filter == EVFILT_WRITE ? evlist[i].ident : ARES_SOCKET_BAD);
}
}
else {
ares_process_fd(channel, ARES_SOCKET_BAD, ARES_SOCKET_BAD);
}
} else {
printf("empty sock!\n");
}

close(kq);

if (socknum <= 0) {
break;
}
}
}
#elif defined(ANDROID)
void main_loop_epoll(ares_channel& channel)
{
int epfd = epoll_create(10);
if (epfd == -1)
{
return;
}

while(1)
{
struct epoll_event evlist[10];

ares_socket_t socks[ARES_GETSOCK_MAXNUM];
int bitmask = ares_getsock(channel, socks, ARES_GETSOCK_MAXNUM);

int socknum = 0;
for (int i = 0; i < ARES_GETSOCK_MAXNUM; i++)
{
struct epoll_event ev;
ev.events = 0;
ev.data.fd = socks[i];

if (ARES_GETSOCK_READABLE(bitmask, i)) {
ev.events |= EPOLLIN;
}
if (ARES_GETSOCK_WRITABLE(bitmask, i)) {
ev.events |= EPOLLOUT;
}

if (ev.events != 0) {
epoll_ctl(epfd, EPOLL_CTL_ADD, socks[i], &ev);
socknum++;
}
else {
break;
}
}

if (socknum > 0) {
timeval tv, * tvp;
int timeout_ms = 1000;
tvp = ares_timeout(channel, NULL, &tv);
if (tvp) {
timeout_ms = (int)(tvp->tv_sec * 1000) + (int)(tvp->tv_usec / 1000);
}

int nev = epoll_wait(epfd, evlist, 10, timeout_ms);
XLogInfo("epoll ret:%d!\n", nev);
if (nev > 0) {
for (int i = 0; i < nev; i++) {
ares_process_fd(channel,
(evlist[i].events & EPOLLIN) ?
evlist[i].data.fd : ARES_SOCKET_BAD,
(evlist[i].events & EPOLLOUT) ?
evlist[i].data.fd : ARES_SOCKET_BAD);
}
} else {
// timeout or epoll error
// dont quit to make sure all query callback
ares_process_fd(channel, ARES_SOCKET_BAD, ARES_SOCKET_BAD);
}

for (int i = 0; i < socknum; i++) {
epoll_ctl(epfd, EPOLL_CTL_DEL, socks[i], NULL);
}

} else {
XLogInfo("empty sock!\n");
break;
}
}

close(epfd);
}
#elif defined(WIN32)
void main_loop_select(channel)
{
int nfds, count;
fd_set readers, writers;
timeval tv, * tvp;

while (1) {
FD_ZERO(&readers);
FD_ZERO(&writers);
nfds = ares_fds(channel, &readers, &writers);
if (nfds == 0) {
break;
}
tvp = ares_timeout(channel, NULL, &tv);
count = select(nfds, &readers, &writers, NULL, tvp);
ares_process(channel, &readers, &writers);
}
}
#else
void main_loop_poll(ares_channel& channel)
{
while (1)
{
ares_socket_t socks[ARES_GETSOCK_MAXNUM];
int bitmask = ares_getsock(channel, socks, ARES_GETSOCK_MAXNUM);

int socknum = 0;
struct pollfd pfd[ARES_GETSOCK_MAXNUM];
for (int i = 0; i < ARES_GETSOCK_MAXNUM; i++) {
pfd[i].events = 0;
pfd[i].revents = 0;
if (ARES_GETSOCK_READABLE(bitmask, i)) {
pfd[i].fd = socks[i];
pfd[i].events |= POLLRDNORM | POLLIN;
}
if (ARES_GETSOCK_WRITABLE(bitmask, i)) {
pfd[i].fd = socks[i];
pfd[i].events |= POLLWRNORM | POLLOUT;
}
if (pfd[i].events != 0)
socknum++;
else
break;
}

int nfds = 0;
if (socknum) {
timeval tv, * tvp;
int timeout_ms = 1000;
tvp = ares_timeout(channel, NULL, &tv);
if (tvp) {
timeout_ms = (int)(tvp->tv_sec * 1000) + (int)(tvp->tv_usec / 1000);
}

int nfds = poll(pfd, socknum, timeout_ms);
if (!nfds) {
ares_process_fd(channel, ARES_SOCKET_BAD, ARES_SOCKET_BAD);
}
else {
/* move through the descriptors and ask for processing on them */
for (int i = 0; i < socknum; i++) {
ares_process_fd(channel,
pfd[i].revents & (POLLRDNORM | POLLIN) ?
pfd[i].fd : ARES_SOCKET_BAD,
pfd[i].revents & (POLLWRNORM | POLLOUT) ?
pfd[i].fd : ARES_SOCKET_BAD);
}
}
}
else {
XLogInfo("empty sock!\n");
break;
}
}
}
#endif

void my_ares_dns_loop(ares_channel& channel)
{
#if defined(__APPLE__)
main_loop_kqueue(channel);
#elif defined(ANDROID)
main_loop_epoll(channel);
#elif defined(WIN32)
main_loop_select(channel);
#else
main_loop_poll(channel);
#endif
}

Android 获取 connectivity manager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static JavaVM *gJVM = NULL;
static jobject gContext = NULL;

// native 层,JNI_OnLoad 时,缓存 JVM
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv *env = NULL;

if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}

gJVM = vm;
ares_library_init_jvm(vm);
return JNI_VERSION_1_4;
}

// native 层,通过 activity 获取 application context, 再以此获取 connectivity manager
JNIEXPORT void JNICALL Java_com_mydnstest_MainActivity_init(
JNIEnv *env,
jobject activity)
{

// 通过 activity 获取 application context (注意保存为全局引用)
jclass cls = env->FindClass("android/content/Context");
jmethodID mid = env->GetMethodID(cls, "getApplicationContext", "()Landroid/content/Context;");
jobject context = env->CallObjectMethod(activity, mid);

gContext = env->NewGlobalRef(context);

env->DeleteLocalRef(context);
env->DeleteLocalRef(cls);
}

// native 层,ares_library_init 之后,利用缓存的 jvm 和 application context 进行 c-ares android 初始化
void my_ares_android_init(JavaVM *vm, jobject android_context)
{
JNIEnv* env; // 通过 JVM 获取当前线程的 env

jclass cls = env->FindClass("android/content/Context");
jmethodID mid = env->GetMethodID(cls, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
jfieldID fid = env->GetStaticFieldID(cls, "CONNECTIVITY_SERVICE", "Ljava/lang/String;");

jstring str = (jstring)env->GetStaticObjectField(cls, fid);
jobject connectivity_manager = env->CallObjectMethod(android_context, mid, str);
if (connectivity_manager == NULL)
return;

ares_library_init_android(connectivity_manager);

env->DeleteLocalRef(connectivity_manager);
env->DeleteLocalRef(str);
env->DeleteLocalRef(cls);
}

注意事项

iOS 弹窗申请本地网络权限

若未设置 DNS Server,则 c-ares 会尝试查询 resolv.conf,失败时,会默认使用 LOOPBACK 作为 DNS 服务器,对于 iOS 平台,与 LOOPBACK 通信会触发本地网络权限。

iOS libresolv 检查失败

若通过 cmake 来生成 ares_config.h,则可能导致 CARES_FUNCTION_IN_LIBRARY (res_servicename resolv HAVE_RES_SERVICENAME_IN_LIBRESOLV) 检测失败,从而未打开 CARES_USE_LIBRESOLV 和 HAVE_LIBRESOLV 宏定义。

c-ares 中 CmakeLists.txt 中判断逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 当使用 ios.toolchain.cmake 配置 iOS 环境时,CARES_FUNCTION_IN_LIBRARY 方法会产生误判,调用 CHECK_FUNCTION_EXISTS (res_servicename) 直接返回 True,从而不再调用 CHECK_LIBRARY_EXISTS(resolv res_servicename)判断 libresolv 的情况。
MACRO (CARES_FUNCTION_IN_LIBRARY func lib var)
SET (_ORIG_CMAKE_REQUIRED_LIBRARIES "${CMAKE_REQUIRED_LIBRARIES}")
SET (CMAKE_REQUIRED_LIBRARIES )
CHECK_FUNCTION_EXISTS ("${func}" "_CARES_FUNC_IN_LIB_GLOBAL_${func}")
SET (CMAKE_REQUIRED_LIBRARIES "${_ORIG_CMAKE_REQUIRED_LIBRARIES}")

IF ("${_CARES_FUNC_IN_LIB_GLOBAL_${func}}")
SET (${var} FALSE)
ELSE ()
CHECK_LIBRARY_EXISTS ("${lib}" "${func}" "" ${var})
ENDIF ()
ENDMACRO ()

# Look for dependent/required libraries
CARES_FUNCTION_IN_LIBRARY (res_servicename resolv HAVE_RES_SERVICENAME_IN_LIBRESOLV)
IF (HAVE_RES_SERVICENAME_IN_LIBRESOLV)
SET (HAVE_LIBRESOLV 1)
ENDIF ()

CHECK_XXX_EXISTS 会调用 try_compile(),构造测试工程编译链接相关库和符号,以此判断库/符号是否存在。try_compile 有两种模式,通过 CMAKE_TRY_COMPILE_TARGET_TYPE 来设置编译目标为 EXECUTABLE 还是 STATIC_LIBRARY。当使用 STATIC_LIBRARY 时,测试工程仅编译,不会触发链接,因此无法正常判断符号存在性,一般情况下使用 EXECUTABLE。

旧版本的 cmake(如 3.18.x)在 CMAKE_TRY_COMPILE_TARGET_TYPE 设置编译目标为 EXECUTABLE 时,生成的 Xcode 工程没有屏蔽签名选项,导致因为缺少 bundleid 等信息无法编译,影响了结果判断。高版本的 cmake (如 3.22.1)会在 Build Settings 中 User-Defined 中增加 CODE_SIGNING_ALLOWED = NO 关闭签名。

使用同一个 channel 发起多个 dns 请求,仅有一个正常回调,其他的在无回调,或在 channel 销毁时返回 ARES_EDESTRUCTION

在 loop 驱动时,每一帧,需要判断当前 socket 的数量,当 socket 数量不为 0 时,无论 poll/epoll/kqueue 的结果是否成功,都要驱动 ares_process_fd。

参考资料

https://github.com/c-ares/c-ares
https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling-toolchain
https://github.com/leetal/ios-cmake