socket编程种名字与地址转换函数

参考:《UNIX 网络编程 · 卷1 : 套接字联网API》

之前都是使用数值地址来表示主机(比如:127.0.0.1),用数值端口号来标识服务器(比如:6379)。

但是有时候最好使用名字而不是数值:名字比较容易记住,数值地址容易变动,而名字地址保持不变;随着 IPv6 上转移,数值地址变得很长,手工键入数值容易出错。之后将有一系列函数用于名字、数值、端口之间的转换。

gethostbyname & gethostbyaddr 函数

gethostbyname 函数

查找主机名字最基本的函数时 gethostbyname。如果调用成功,就返回一个指向 hostent 结构体的指针,该结构体中含有查找主机的所有 IPv4 地址。这个函数局限就是只能返回 IPv4 的地址,之后提到的 getaddrinfo 函数将能同时处理 IPv4 和 IPv6 地址,POSIX 预警在将来的某个版本将撤销 gethostbyname 函数,然而 IPv6 能被广泛使用还在遥远的将来,那么在新的程序中应该使用 getaddrinfo 函数。

gethostbyname 函数定义:

#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);

本函数返回的空指针指向如下的 hostent 结构:

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 from name server.  */
};

gethostbyname 执行的是对 A 记录的查询,只能返回 IPv4 地址,其结构信息如下如所示:

hostent结构信息

geohostbyname 和其他套接字函数不同:发生错误时,不设置 errno 变量,而是将全局整数变量 h_errno 设置为在头文件 <netdb.h> 中定义的下列常值之一:

HOST_NOT_FOUND、TRY_AGAIN、NO_RECOVERY、NO_DATA(等同于NO_ADDRESS)。

多数解析器提供名为 hstrerror 的函数,它以某个 h_errno 值作为唯一的参数,返回的是一个 const char* 指针,指向相应的错误说明。

如下的例子调用 gethostbyname,显示所有信息:

#include <unistd.h>
#include <netdb.h>
#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    char *ptr, **pptr;
    char str[INET_ADDRSTRLEN];
    struct hostent *hptr;
    while (--argc > 0)
    {
        ptr = *++argv;
        if ((hptr = gethostbyname(ptr)) == NULL)
        {
            printf("gethostbyname error for host: %s:%s.\n", ptr, hstrerror(h_errno));
            continue;
        }
        printf("official hostname: %s\n", hptr->h_name);
        for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)
        {
            printf("\talias: %s\n", *pptr);
        }
        switch (hptr->h_addrtype)
        {
        case AF_INET:
            pptr = hptr->h_addr_list;
            for (; *pptr != NULL; pptr++)
            {
                printf("\taddress: %s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
            }
            break;
        default:
            printf("unknow address type.");
            break;
        }
    }
    return 0;
}

运行上面的程序时,后面的参数跟上主机名,就能打印出上图 hostent 结构体的信息。

gethostbyaddr函数

该函数试图由一个二进制的 IP 地址找到对应的主机名,与 gethostbyname 的行为刚好相反。

#include <netdb.h>
struct hostent *gethostbyaddr(const char* addr, socklen_t len, int family);

参数:

addr:实际上不是 char* 类型,是一个指向存放 IPv4 地址的某个 in_addr 结构的指针。

len:是这个结构的大小。

falily:对于 IPv4 来说,是 AF_INET。

返回值:

指向与之前描述同样的 hostent 结构的指针,在上面的 gethostbyhost 已经描述过。通常感兴趣的字段是存放规范主机名的 h_name。

注意:由于历史原因这两个函数是不可重入的,因为因为它们返回指向同一个静态结构的指针。之后的版本实现了多线程版本,它们的名字都是该函数名加_r结尾的。

getservbyname & getservbyport 函数

getservbyname函数

和主机一样,服务器也通常靠名字来认知。如果我们在代码中通过其名字而不是端口号来指代一个服务,而且从名字到端口号的映射关系保存在一个文件中(通常是 /etc/services),那么即使端口号发生变动,我们仅需就改的是 /etc/service 文件中的某一行,而不用重新编译程序。getservbyname 函数用于根据名字查找相应服务。

#include <netdb.h>
struct servent* getservbyname(const char* servname, const char* protoname);

参数:

servname:服务器名字。

protoname:协议。如果指定了 proto 参数,那么指定服务必须有匹配的协议。游戏因特网服务即用 TCP 也用 UDP,有的则仅支持单个协议。如果 protoname 未指定而 servname 指定服务支持多个协议,那么返回哪个端口号取决于实现。

返回值:

成功返回指向 struct servent 结构体的指针。出错返回 NULL。

本函数返回的非空指针指向如下的 servent 结构:

struct servent
{
    char *s_name;		/* Official service name.  */
    char **s_aliases;	/* Alias list.  */
    int s_port;			/* Port number.  */
    char *s_proto;		/* Protocol to use.  */
};

struct servent 结构体中我们关心的主要字段是端口号。既然端口号是以网路字节序返回的,把它存放到套接字地质结构时绝不能调用 htons。

本函数典型调用如下:

struct servaddr* sptr;
sptr = getservbyname("domain", "udp");
sptr = getservbyname("ftp", "tcp");	//FTP仅使用支持TCP
sptr = getservbyname("ftp", NULL);	//和上面的等价
sptr = getservbyname("ftp", "udp");	//调用失败

getservbyport函数

用于根据给定的端口号和可选协议查找响应服务。

#include <netdb.h>
struct servent* getservbyport(int port, const char* protoname);

参数:

prot:端口。值必须是网络字节序

protoname:协议。和上面 getservbyport 描述的相同。

返回值:

成功返回指向 struct servent 结构体的指针,出错返回 NULL。

本函数典型调用如下:

struct servent* sptr;
sptr = getservbyport(htons(53), "udp");	//DNS
sptr = getservbyport(htons(21), "tcp");	//TCP
sptr = getservbyport(htons(21), "NULL");//TCP, 和上面相等
sptr = getservbyport(htons(21), "udp"); //调用失败,UPD没有服务使用21端口

getaddrinfo & getnameinfo 函数

getaddrinfo 函数

由于 gethostbyname 和 gethostbyaddr 函数仅支持 IPv4,又想要解析 IPv6 就需要 getaddrinfo 函数。getaddrinfo 函数能够处理名字到地址,以及服务到端口的两种转换,返回的是一个 sockaddr 结构体而不是一个地址列表。

这些 sockaddr 结构体随后可以由套接字函数直接使用。该函数是协议无关的。定义如下:

#include <netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hists, struct addrinfo** result);

参数:

result:该函数通过 result 指针参数返回一个指向 addrinfo 结构链表的指针,addrinfo 的结构定义如下:

struct addrinfo
{
    int ai_flags;				/* Input flags.  */
    int ai_family;				/* Protocol family for socket.  */
    int ai_socktype;			/* Socket type.  */
    int ai_protocol;			/* Protocol for socket.  */
    socklen_t ai_addrlen;		/* Length of socket address.  */
    struct sockaddr *ai_addr;	/* Socket address for socket.  */
    char *ai_canonname;			/* Canonical name for service location.  */
    struct addrinfo *ai_next;	/* Pointer to next in list.  */
};

hostname:主机名或 IPv4 或 IPv6 数串。

service:是一个服务名或十进制端口号数串。

hints:可以是空指针,也可以指向某个 addrinfo 结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。如某个服务即支持 TCP 又支持 UDP,如果将 hints 结构体中 ai_socktype 成员设置成为 SOCK_DGRAM,使得返回的仅仅是适用于数据报套接字的信息。

hints 结构体中调用者可以设置的成员有:

  • ai_flags(零个或多个或在一起的AI_xxx值)
  • ai_family(某个AF_xxx值)
  • ai_socktype(某个sock_xxx值)
  • ai_protocol

其中 aiflags 成员可用的标志值及其含义如下:

  • AI_PASSIVE 套接字将用于被动打开。

  • AI_CANONNAME 告知 getaddrinfo 函数返回主机的规范名字。

  • AI_NUMERICHOST 防止任何类型的名字到地址映射,hostmame 参数必须是一个地址串。

  • AI_NUMERICSERV 防止任何类型的名字到服务映射,service 参数必须是一个十进制端口号数串。

  • AI_V4MAPPED 如果同时指定 ai_family 成员的值为 AF_INET6,那么如果没有可用的 AAAA 记录,就返回与 A 记录对应的 IPv4 映射的 IPv6 地址。

  • AI_ALL 如果同时指定 AI_V4MAPPED 标志,那么除了返回与 AAAA 记录对应的 IPv6 地址外,还返回与 A 记录对应的 IPv4 映射的 IPv6 地址。

  • AL_ADDRCONEIG 按照所在主机的配置选择返回地址类型,也就是只查找与所在主机问馈接口以外的网络接口配置的 IP 地址版本一致的地址。

返回值:

出错返回非 0。成功返回 0,由 result 参数指向的变量已被填入一个指针,它指向的是由其中的 ai_next 成员串起来的 addrinfo 结构体链表。导致返回多个 addrinfo 结构情况如下:

  1. 如果与 hostname 参数关联的地址有多个,适用于所请求地址族的每个地址都返回一个对应的结构。
  2. 如果 sevice 参数指定的服务支持多个套接字类型,那么每个套接字类型都可能返回一个对应的结构体,取决于 hints 结构中的 ai_socktype 成员。

这些结构的返回顺序没有保证。如果 hints 结构体中设置了 AI_CANONNAME 标志,那么本函数返回的第一个 addrinfo 结构的 ai_canonname 成员指向所查找主机的规范名字。

执行下列程序片,假设主机 freebsd4 的规范名字是 freebsd4.unpbook.com,并且它在 DNS 中有两个 IPv4 地址:

struct addrinfo hints, *res;
bzero(&hints, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = AF_INET;
getaddrinfo("freebsd4", "domain", &hints, &res);

返回的信息如下:

getaddrinfo返回的信息

getnameinfo 函数

是作为 getaddrinfo 的互补函数,它以一个套接字地址作为参数,返回描述符中主机的一个字符串和描述符其中的服务的另一个字符串。该函数同样是协议无关的。

#include <netdb.h>
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);

参数:

sockaddr:指向一个套接字地址结构。

addrlen:这个地址结构的长度。该结构的长度通常由 accept、recvfrom、getsockname 或 getpeername 返货。

host:主机字符串。(调用者预先分配存储空间)

hostlen:主机字符串长度。(如果不想返回字符串,指定为 0)

serv:服务字符串。(调用者预先分配存储空间)

servlen:服务字符串长度。(如果不想返回字符串,指定为 0)

flats:可指定标志,用于改变 getnameinfo 的操作(如:NI_DGRAM 数据报服务)

返回值:

成功返回 0,出错为非 0。

该函数与 sock_ntop 的差别在于,sock_ntop 不涉及 DNS,只返回 IP 地址和端口号的一个可显示版本,而该函数通常尝试获取主机的服务的名字。

gai_strerror 函数

该函数用于返回 getaddrinfo 函数的非 0 错误所指向的对应的出错信息字符串的指针。

#include <netdb.h>
const char* gai_strerror(int errno);

getaddrinfo 返回的非 0 错误如下:

常值说明
EAI_AGAIN名字解析中临时失败
EAI_BADFLAGSai_flags的值无效
EAI_FAIL名字解析中不可恢复地失败
EAL_FAMTLY不支持ai_family
EAI_MEMORY内存分配失败
EAI_NONAMEhostname或service未提供,或者不可知
EAL_OVERFLOW用户参数缓冲区溢出(仅限getnameinfo()函数)
EAI_SERVICE不支持ai_socktype类型的service
EAI_SOCKTYPE不支持ai_socktype
EAI_SYSTEM在errno变量中有系统错误返回

freeaddrinfo 函数

由 getaddrinfo 返回的所有存储空间都是动态获取的,包括 addrinfo 结构、ai_addr 结构和 ai_canonname 字符串。这些空间需要调用 freeaddrinfo 函数返还给操作系统。

#include <netdb.h>
void freeaddrinfo(struct addrinfo* ai);

参数:

ai:指向由 getaddrinfo 返回的第一个 addrinfo 结构。这个链表中所有的结构以及由它们指向的任何动态存储空间都被释放。

可重入函数讨论

一般在设计多线程时,会讨论可重入函数问题。

由于历史原因,gethostbyname、gethostbyaddr、getservbyname 和 getservbyport 这 4 个函数是不可重入的因为它们都返回指向同一个静态结构的指针。新版本系统支持多线程的实现了这 4 个函数的可重入版本,是以它们的名字以_r结尾。inet_pton 和 inet_ntop 总是可重入的。

因历史原因,inet_ntoa 是不可重入的,不过支持线程的一些实现提供了使用线程特定数据的可重入版本。

getaddrinfo 可重入的前提是由它调用的函数都可重入,这就是说,它应该调用可重入版本的 gethostbyname(以解析主机名)和 getservbyname(以解析服务名)。本函数返回的结果全部存放在动态分配内存空间的原因之一就是允许它可重入。

getnameinfo 可重入的前提是由它调用的函数都可重入,这就是说,它应该调用可重入版本的 gethostbyaddr(以反向解析主机名)和 getservbyport(以反向解析服务名)。它的 2 个结果字符串(分别为主机名和服务名)由调用者分配存储空间,从而允许它可重入。

errno 变量存在类似的问题,这个整形变量每个进程一个副本。但是一个进程中,有多个线程,一个线程中发生了错误并色湖之了 errno 的值,从系统调用返回时把错误码存入 errno 到稍后由程序显示 errno 的值之间存在一个小的时间窗口,如果这时候另一个线程改变了 errno 的值,就会影响其他线程。

所以在可重入的函数中不调用任何不可重入的函数,后面章节介绍多线程如何处理 errno 变量问题。

gethostbyname_r & gethostbyaddr_r 函数

有两种方法可以把诸如 gethostbyname 之类的不可重入的函数改为可重入。

  1. 把由不可重入函数填写并返回静态结构的做法改为由调用这分配再由可重入函数填写结构。这就要新增三个参数:指向 hostent 结构体的指针,指向存放多有其他信息所用缓冲区的指针和其大小,还有存放错误码的变量,因为不能再用全局 h_errno。

    这种方法 gatename 和 inte_ntop 也使用这种方法。

  2. 由可重入函数调用 malloc 动态分配空间。这是 getaddrinfo 使用的技巧。问题是调用该函数的应用进程必须手动释放分配的内存空间,否则内存泄漏。

可重入版本的函数定义如下:

#include <netdb.h>

struct hostent *gethostbyname_r(const char *hostname, struct hostent *result, char *buf, int buflen, int *h_errnop); 

struct hostent *gethostbyaddr_r(const char *addr, int len, int type, struct hostent *result, char *buf, int buflen, int *h_errnop);

每个函数都需要 4 个额外的参数。其中 result 参数指向由调用者分配并由被调用函数填写的 hostent 结构。成功返回时本指针同时作为函数的返回值。

buf 参数指向由调用者分配且大小为 buflen 的缓冲区。该缓冲区用于存放规范主机名、别名指针数组、各个别名字符串、地址指针数组以及各个实际地址。由 result 指向的 hostent 结构中的所有指针都指向该缓冲区内部。那这个缓冲区要有多大才行呢?不幸的是,就该缓冲区的大小而言,大多数手册页面只是含糊地说“该缓冲区必须大得足以存放与 hostent 结构关联的所有数据”。gethostbyname 当前的实现最多能够返回 35 个别名指针和 35 个地址指针,并内部使用一个 8192 字节的缓冲区存放这些别名和地址。因此大小为 8192 字节的缓冲区应该足够了。

如果出错,错误码就通过 h_errnop 指针而不是全局变量 h_errno 返回。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值