Feb 4

探探gethostbyname 不指定

felix021 @ 2013-2-4 23:18 [IT » 网络] 评论(0) , 引用(0) , 阅读(6338) | Via 本站原创
开篇先说一下,在它的manpage里面,有这么一句话:
引用
The gethostbyname*() and gethostbyaddr*() functions are obsolete. Applications should use getaddrinfo(3) and getnameinfo(3) instead.
这是因为gethostbyname只能处理ipv4请求(经测试ipv6.google.com不能解析,ipv6.tsinghua.edu.cn只能解析出ipv4地址),因此推荐使用 getaddrinfo 来替代它。

忽略上述问题不管的话, gethostbyname 还是有点意思的。它的原型和用法大概可以这样:
struct hostent *gethostbyname(const char *name);

struct hostent *he = gethostbyname(argv[1]);
printf("%s\n", inet_ntoa(*(struct in_addr *)he->h_addr));

用法特别简单吧,如果不是在提到这个函数的时候附带说明“它使用了内部分配的空间,因此不是线程安全的”,估计会大受欢迎的。
//注:由于windows版用了 thread local 的变量,所以在windows下它是线程安全的(只要不是同一个线程马上再调用覆盖它的话)。

相应的,像ctime、localtime之类的posix接口一样,它的线程安全版就是带了 _r 后缀的 gethostname_r ,虽然名字只复杂了一点点,但是接口可不只是复杂了一点点啊:
int gethostbyname_r(const char *name, struct hostent *ret,
    char *buf, size_t buflen, struct hostent **result, int *h_errnop);
其中的name是hostname(或者ip地址),ret应当指向一个分配好的struct hostent结构体(函数把内容填进去),buf应当指向一块分配好的空间(不小于buflen),result是一个struct hostent的二级指针(如果失败会存个NULL),h_errnop则指向某个int(失败则填对应的herr,这个奇葩函数的errno跟errno.h里面那个还不是一套)。

用起来真是相当地恶心,比如下面这个函数通过它来解析ip地址,跟前面的代码的复杂度完全不是一个量级的:
int host2addr(const char *host, struct in_addr *addr)
{
    struct hostent he, *result;
    int herr, ret, bufsz = 512;
    char *buff = NULL;
    do {
        char *new_buff = (char *)realloc(buff, bufsz);
        if (new_buff == NULL) {
            free(buff);
            return ENOMEM;
        }
        buff = new_buff;
        ret = gethostbyname_r(host, &he, buff, bufsz, &result, &herr);
        bufsz *= 2;
    } while (ret == ERANGE);

    if (ret == 0 && result != NULL)
        *addr = *(struct in_addr *)he.h_addr;
    else if (result == NULL)
        ret = herr;
    free(buff);
    return ret;
}


代码是写出来了,但是觉得不太对头,仔细看了一下 struct hostent 的定义,里头还有多个指针(h_name、h_aliases、h_addr_list)什么的:
struct hostent {
  char  *h_name;            /* official name of host */
  char **h_aliases;        /* alias list */
  int    h_addrtype;        /* host address type */
  int    h_length;          /* length of address */
  char **h_addr_list;      /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */
如果不释放的话,不会造成内存泄漏么?

于是去看了下gethostbyname和gethostbyname_r的源码,进入 eglibc-2.15 的源码, ctags -r 然后 vim -t gethostbyname ,竟然没有这个函数。然后翻了翻,看到有个 inet 目录,里面有个 gethstbynm.c 这样一个奇葩的文件,里面除了include一些头文件之外,只有一些简单的宏定义和一个奇怪的include:
#define LOOKUP_TYPE struct hostent
#define FUNCTION_NAME  gethostbyname
#define DATABASE_NAME  hosts
#define ADD_PARAMS  const char *name
#define ADD_VARIABLES  name
#define BUFLEN      1024
#define NEED_H_ERRNO    1

#define HANDLE_DIGITS_DOTS  1

#include <nss/getXXbyYY.c>

打开一看 getXXbyYY.c 这个文件更奇葩了,根本就是用宏来实现C++中的模板功能啊。。。grep一下还发现,这个文件被18个其他文件include,也就是说,有18个函数(诸如gethostbyaddr, getnetbyaddr, 甚至getpwuid、getpwnam)都是用的这个模板。

凑合着看了一下,代码倒是不复杂(具体就不贴出来了),基本逻辑是这样的:
LOOKUP_TYPE *
FUNCTION_NAME (ADD_PARAMS)
{
    获取锁
    如buffer==NULL,分配初始化的空间
    如定义了HANDLE_DIGITS_DOTS(如gethostbyname)则检查name是否是ip地址并处理(是的话就不用发dns请求了)
    类似的while循环:调用对应的线程安全版函数,并检查是否buffer大小不够(如果是的话realloc)
    释放锁
    如果定义了NEED_H_ERRNO,设置对应的值
    返回
}

也就是说,代码流程基本上跟前面调用 gethostbyname_r 的代码一样,只是多了一个获取/释放锁的操作。根据里面的流程可以看出,代码里面不是使用静态的空间,而是动态malloc的(所以要获取锁,否则在libc内部崩溃,那不好)。由此,所谓的不是线程安全的,其实并不是函数本身,而是返回的数据。

但是,看懂了这个代码,并没有解决上面的问题啊。。。于是到 stackoverflow 上面去提了个问,很快就得到大牛的回答了:
引用
You should print out the values of the pointers in that struct to find out the answer to your question. You'll discover that they all point to data inside the buffer you allocated.

So a single free is all you need to free up all the memory.

But this also means that you must not free that allocation until you've finished using or copying whatever data you're interested in.

也就是说,我完全忽略了塞给 gethostbyname_r 的 buf 这个参数:hostent 里面的指针指向的数据就存在 buf 里面,因此最后只要 free 掉它就够了。

由于有些域名可能绑定了好多个ip(比如google.com可能就绑了数十个),buf 的空间可能不够大,所以才需要一个realloc的机制。根据实测,buf的大小从512或者1024开始比较合适,普通的域名就不需要realloc了。

到此差不多该结束了,最后吐槽一句,这两个接口真奇葩啊。。。
分页: 1/1 第一页 1 最后页 [ 显示模式: 摘要 | 列表 ]