伯阳的网络笔记:获取 DNS 地址时,res_nclose 为什么不够
伯阳的网络笔记(十):如何正确的获取 DNS 地址
在 iOS 里,如果想拿到当前系统配置的 DNS 地址,很多人会直接使用 libresolv 里的接口。代码看起来不复杂,但这里其实藏着一个很容易忽略的小坑。
先在系统中加入 libresolv.tbd 库。
然后在头文件中引入以下几个头文件:
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <resolv.h>
#include <dns.h>
然后使用以下代码,或者以此进行改编:
NSMutableArray *DNSList = [NSMutableArray array];
res_state res = malloc(sizeof(struct __res_state));
int result = res_ninit(res);
if (result == 0) {
for (int i=0;i < res->nscount;i++) {
NSString *s = [NSString stringWithUTF8String:inet_ntoa(res->nsaddr_list[i].sin_addr)];
[DNSList addObject:s];
}
}
res_nclose(res);
free(res);
// dnsList 就是 DNS 服务器地址
但不巧的是,有朋友提醒我,这段代码其实会造成内存泄漏。我自己试了一下,还真有。那问题到底出在哪?
查找过程
经过初步排查,发现问题出在 int result = res_ninit(res); 这一步创建出来的内部资源没有被正确释放。
但奇怪的是,下面明明已经调用了 res_nclose(res);。
没办法,只能继续顺着 res_ninit / res_nclose 往下翻资料。
终于在经过了漫长的翻阅资料之后,发现了一份 Oracle 的文档。
里面有几段注释非常关键:
-
The res_ndestroy() function should be called to free memory allocated by res_ninit() after the last use of statp.
-
The res_nclose() function closes any open files referenced through statp.
-
The res_ndestroy() function calls res_nclose(), then frees any memory allocated by res_ninit() referenced through statp.
以及下面这段示例代码:
#include <resolv.h>
#include <string.h>
int main(int argc, char **argv)
{
int len;
struct __res_state statp;
union msg {
uchar_t buf[NS_MAXMSG];
HEADER h;
} resbuf;
/* Initialize resolver */
memset(&statp, 0, sizeof(statp));
if (res_ninit(&statp) < 0) {
fprintf(stderr, "Can't initialize statp.\n");
return (1);
}
/*
* Turning on DEBUG mode keeps this example simple,
* without need to output anything.
*/
statp.options |= RES_DEBUG;
/* Search for A type records */
len = res_nsearch(&statp, "example.com", C_IN, T_A,
resbuf.buf, NS_MAXMSG);
if (len < 0) {
fprintf(stderr, "Error occured during search.\n");
return (1);
}
if (len > NS_MAXMSG) {
fprintf(stderr, "The buffer is too small.\n");
return (1);
}
/* ... Process the received answer ... */
/* Cleanup */
res_ndestroy(&statp);
return (0);
}
意思其实很明确:
DNS的相关状态会存储在statp中,res_ninit会初始化这份 resolver state,之后就可以从中读取数据。res_nclose只负责关闭statp里相关的打开资源,但并不会释放res_ninit额外申请的那部分内存。- 如果既想关闭资源、又想把
res_ninit申请的内部内存一起释放掉,就应该调用res_ndestroy。
也就是说,这里的泄漏并不只是 res_state 这个结构体本身,而是 res_ninit 在内部为 resolver state 申请的附加资源,没有被 res_nclose 一起释放掉。
正确代码
NSMutableArray *DNSList = [NSMutableArray array];
res_state res = malloc(sizeof(struct __res_state));
int result = res_ninit(res);
if (result == 0) {
for (int i=0;i < res->nscount;i++) {
NSString *s = [NSString stringWithUTF8String:inet_ntoa(res->nsaddr_list[i].sin_addr)];
[DNSList addObject:s];
}
}
res_ndestroy(res);
free(res);
// dnsList 就是 DNS 服务器地址
如果你用的是栈上 struct __res_state,结论也是一样的:
即使不需要 free 结构体本身,依然要记得调用 res_ndestroy(&statp) 去清理 res_ninit 内部分配的资源。