您的位置:首页 > 理论基础 > 计算机网络

主机名与网络名查询gethostbyname,gethostbyaddr 与 高级名字与地址转换————getaddrinfo()函数详解 .

2011-08-04 20:56 645 查看
主机名与网络名查询

在这一章,我们将会了解以下内容:

如何确定我们的本地主机名

如何将主机名解析为IP地址

如何将IP地址解析为主机名

一旦我们学完这一章,我们就可以在我们的客户端与服务器程序中使用主机名或是IP地址。

理解名字的需要

人们喜欢使用和记住名字,而不是IP地址。在网络世界中,名字实际上为我们解决了许多问题:

他们为一个网站提供了人类友好的引用

他们可以允许IP地址改变,而名字保持不变

他们允许为同一个主机或是服务指定多个IP地址

我们已经理解比起IP地址来,名字提供了更为简单的引用。然而,另外一点就是名字可以保持不变,而允许主机的IP地址发生变化。IP地址的变化通常是因为网络的变化,ISP的变化,设备的变化等。只要我们记住了网络站点的名字,我们就可以不必关心其实际的IP地址。

最后一点是简单的被轻视了。查看ftp.redhat.com,我们会得到下面的两个IP地址:

•208.178.165.228

•206.132.41.212

我们不必在意这两个IP地址是指向同一个ftp主机或是为负载平衡的目的而设的两个不同的镜像站点。事实是,通过使用任何一个IP地址,我们可以得到我们希望的同一个文件。

这就引入了这一章名字解析的主题。首先,我们将会学到如何查看本地系统的信息。然而我们将会学到如何使用远程主机名,如何查询, 如何将其转换为IP地址。

使用uname(2)函数

我们要知道的一个有用的函数就是uname(2)函数。这个函数会告诉我们运行我们程序的系统的相关信息。这个函数的原型如下:

#include <sys/utsname.h>

int uname(struct utsname *buf);

这个函数将返回信息存放在结构buf中。当函数成功时会返回0,当发生错误时会返回-1。外部变量errno将会包含错误号。

struct utsname的定义如下所示:

#include <sys/utsname.h>

struct utsname {

char sysname[SYS_NMLN];

char nodename[SYS_NMLN];

char release[SYS_NMLN];

char version[SYS_NMLN];

char machine[SYS_NMLN];

char domainname[SYS_NMLN];

};

结构成员描述如下:

成员 描述

sysname 代表正在使用的操作系统。对于Linux而言,这个值为C字符串"Linux"。

nodename 代表机器的网络节点主机名。

release 操作系统发布号。

version 操作系统版本号。对Linux而言,这代表内核构建的版本号,日期以及时间戳。

machine 代表主机的硬件类型。例如"i686"代表一个奔腾CPU。

domainname 返回主机的NIS/YP域名。

下面的这个例子程序允许我们测试由uname返回的信息。这个程序调用uname函数并且显示在结构utsname中返回的信息。

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#include <errno.h>

#include <sys/utsname.h>

int main(int argc,char **argv)

{

int z;

struct utsname u_name;

z = uname(&u_name);

if(z==-1)

{

fprintf(stderr,"%s:uname(2)/n",strerror(errno));

exit(1);

}

printf(" sysname[] = '%s';/n",u_name.sysname);

printf(" nodename[] = '%s';/n",u_name.nodename);

printf(" release[] = '%s';/n",u_name.release);

printf(" version[] = '%s';/n",u_name.version);

printf(" machine[] = '%s';/n",u_name.machine);

printf(" domainname[] = '%s';/n",u_name.domainname);

return 0;

}

这个函数的运行结果如下:

@tux

$ ./uname

sysname[] = 'Linux';

nodename[] = 'tux';

release[] = '2.2.10';

version[] = #1 Sun Jul 4 00:28:57 EDT 1999';

machine[] = 'i686';

domainname[] = '';

取得主机名与域名

函数gethostname(2)与getdomainname(2)是另外两个可以用来得到当前系统的函数。

使用gethostname(2)函数

gethostname函数可以用来确定当前的主机名。这个函数的概要如下:

#include <unistd.h>

int gethostname(char *name, size_t len);

这个函数需要两个参数:

接收缓冲区name,其长度必须为len字节或是更长

接收缓冲区name的最大长度

如果函数成功,则返回0。如果发生错误则返回-1。错误号存放在外部变量errno中。

使用getdomainname(2)函数

getdomainname是中一个方便的函数,可以允许程序获得程序正运行的主机的NIS域名。函数概要如下:

#include <unistd.h>

int getdomainname(char *name,size_t len);

这个函数的用法也gethostname相同。

Linux手册页表明getdomainname函数内部使用e函数来得到并返回NIS域名。

测试getdomainname与gethostname函数

下面这个程序演示了这两个函数的用法。这个程序只是简单的调用这两个函数并报告其结果。

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#include <errno.h>

int main(int argc,char **argv)

{

int z;

char buf[32];

z = gethostname(buf,sizeof buf);

if(z==-1)

{

fprintf(stderr,"%s:gethostname(2)/n",strerror(errno));

exit(1);

}

printf("host name = '%s'/n",buf);

z = getdomainname(buf,sizeof buf);

if(z==-1)

{

fprintf(stderr,"%s:getdomainname(2)/n",strerror(errno));

exit(1);

}

printf("domain name = '%s'/n",buf);

return 0;

}

这个程序的运行结果如下:

$ ./gethostn

host name = 'tux'

domain name = ''

在了解了如何获得本地系统的信息以后,现在我们就可以将我们的注意力转移到解析远程主机名上了。

解析远程地址

将一个名字转换为IP地址的过程是相当复杂的。他涉及到我们本地系统上/etc目录中的许多文件,包括/etc/resolv.conf,/etc/hosts,/etc/nsswitch.conf文件。依据我们的系统是如何配置的,同时也会涉及到其他的文件以及守护进程。例如,在查询这些文件以后,会查询一个名字服务器,这个名字服务器会向前查询其他的名字服务器。所有这些复杂的细节是我们编写程序时不想考虑的内容。

幸运的时,程序的编写者可以像驼鸟那样将头伸在沙中。如果系统进行正确的配置,一些系统函数调用就将是程序员所需要的全部内容。下面的内容就是一些相关的函数集合,这些函数为我们隐藏了远程名字查询的复杂。

错误报告

我们将会描述的这些函数使用一个不同的变量来进行错误报告。在通常的C库函数中,错误代码将会存放在变量errno中。然而这一部分的函数将错误代码存放在变量h_errno中。其概要如下:

#include <netdb.h>

extern int h_errno;

h_errno是一个外部整型变量。错误号会由下列函数传送到变量h_errno中:

gethostbyname(3)

gethostbyaddr(3)

下列函数使用h_errno的值作为输入:

herror(3)

hstrerror(3)

报告一个h_errno错误

正如我们已经知道的,strerror函数方便的将一个errno值转换为一个人类可读的错误信息。相类似的,也存在两个函数用来报告h_errno值:

#include <netdb.h>

extern int h_errno;

void herror(const char *msg);

const char *hstrerror(int err);

函数herror(3)与函数perror(3)相类似。现在认为herror函数是陈旧的,但是我们会在已存在的代码中发现他。他们打印出消息msg,然后是错误原因。这些会被写入标准错误输出流中。

hstrerror(3)函数模仿相似的strerror(3)函数的功能。接受h_errno作为输入值,他会返回一个指向错误信息的指针。返回的指针直到下次调用这个函数之前都是可用的。

理解错误代码

h_errno变量所用的C宏本质上与errno的值不同。下表列出当调用gethostbyname(3)和gethostbyaddr(3)函数时可能会遇到的错误代码。

错误宏 描述

HOST_NOT_FOUND 指定的主机名未知

NO_ADDRESS 指定的主机名可用,但是没有IP地址

NO_DATA 与NO_ADDRESS相同

NO_RECOVERY 发生了一个无法恢复的名字服务器错误

TRY_AGAIN 稍后重试此次操作

在这里要注意,TRY_AGAIN错误代码代表一个也许会被重试努力覆盖的条件;NO_RECOVERY错误代码代表一个不可重试的名字服务器错误,因为在这种条件下不可进行修复;NO_RECOVERY(NO_DATA)错误代码表明已知查询的主机,但是却没有为其定义IP地址;最后,HOST_NOT_FOUND错误代码表明查询的名字不可知。

使用gethostbyname(3)函数

这是我们在这一章将会学习的重要的一个函数。这个函数接受我们希望解析的主机名,然后返回一个以各种方式标识的结构。这个函数的概要如下:

#include <netdb.h>

extern int h_errno;

struct hostent *gethostbyname(const char *name);

函数gethostbyname接受一个代表我们希望解析为地址的主机名的C字符串作为输入参数。如果函数调用成功会返回一个指向hostent结构的指针。如果函数失败,则会返回一个NULL指针,而错误原因将会存放在变量h_errno中。

hostent结构如下:

struct hostent {

char *h_name;

char **h_aliases;

int h_addrtype;

int h_length;

char **h_addr_list;

};

#define h_addr h_addr_list[0]

当我们进行套接口编程时,经常使用这个结构就会对其逐渐熟悉起来了。

结构成员描述

h_name

hostent结构中的h_name成员是我们正在查询的主机的官方名字。也就是主机的权威名字。如果我们提供了一个别名,或是不带域名的主机名,那么这个成员就会描述我们要查询的正确名字。这个成员对于显示或是将结果记入日志文件是相当有用的。

h_aliases成员

返回结构的h_aliases成员是我们查询的主机名的别名数组。这个列表的结尾被标记为NULL指针。作为例子,www.lwn.net的别名列表如下所示:

struct hostent *ptr;

int x;

ptr = gethostbyname("www.lwn.net");

for ( x=0; ptr->h_aliases[x] != NULL; ++x )

printf ("alias = '%s'/n", ptr->h_aliases[x]);

在上面的例子中并没有错误检查。如果ptr为NULL,就表明没有可用的信息。

h_addrtype成员

在成员h_addrtype中返回的值为AF_INET。然而,因为IPv6已经完全实现,名字服务器也会返回IPv6地址。当这种情况发生时,h_addrtype就会在合适的时候返回AF_INET6。

h_addrtype值的上的就是表明在列表h_addr_list中的地址格式。

h_length成员

这个值与h_addrtype成员相关。对于当前的TCP/IP协议版本(IPv4),这个成员的值总是为4,表明4个字节的IP地址。然而,当IPv6实现时,这个值将会是16,并且返回IPv6地址。

h_addr_list成员

当执行一个名字到IP地址的转换时,这个成员就会成为我们最重要的信息。当h_addrtype成员包含AF_INET时,这个数组中的每一个指针指向一个4字节的IP地址。这个列表的结尾被标记为NULL指针。

使用gethostbyname(3)函数

在下面的例子程序中演示了gethostbyname的用法。这个程序会在命令行接收多个主机名,然后分别查询每一个。所有可用的信息都会发送到标准输出,如果名字没有解析将会报告错误。

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <errno.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <netdb.h>

extern int h_errno;

int main(int argc,char **argv)

{

int x,x2;

struct hostent *hp;

for (x=1;x<argc;++x)

{



hp = gethostbyname(argv[x]);

if(!hp)

{



fprintf(stderr,"%s:host '%s'/n",

hstrerror(h_errno),argv[x]);

continue;

}



printf("Host %s: /n",argv[x]);

printf("/tOfficially:/t%s/n",

hp->h_name);

fputs("/tAliases:/t",stdout);

for(x2=0;hp->h_aliases[x2];++x2)

{

if(x2)

fputs(", ",stdout);

fputs(hp->h_aliases[x2],stdout);

}

fputc('/n',stdout);

printf("/tType:/t/t%s/n",

hp->h_addrtype == AF_INET

?"AF_INET"

:"AF_INET6");

if(hp->h_addrtype == AF_INET)

{

for(x2=0;hp->h_addr_list[x2];++x2)

printf("/tAddress:/t%s/n",

inet_ntoa(*(struct in_addr *)hp->h_addr_list[x2]));

}

putchar('/n');

}

return 0;

}

这个函数的执行结果如下:

$ ./lookup www.lwn.net sunsite.unc.edu ftp.redhat.com

Host www.lwn.net :

Officially: lwn.net

Aliases: www.lwn.net

Type: AF_INET

Address: 206.168.112.90

Host sunsite.unc.edu :

Officially: sunsite.unc.edu

Aliases:

Type: AF_INET

Address: 152.2.254.81

Host ftp.redhat.com :

Officially: ftp.redhat.com

Aliases:

Type: AF_INET

Address: 206.132.41.212

Address: 208.178.165.228

gethostbyaddr(3)函数

有时我们知道一个IP地址,但是我们报告主机,而不是IP地址。一个服务器也许需要记录与其连接的客户端主机名,而不仅仅是IP地址。gethostbyaddr函数概要如下:

#include <sys/socket.h>

struct hostent *gethostbyaddr(

const char *addr,

int len,

int type);

gethostbyaddr函数接受三个输入参数:

1 要转换为主机名的输入地址(addr)。对于AF_INET地址类型,这是指向地址结构中的sin_addr成员。

2 输入地址的长度。对于AF_INET类型,这个值为4;而对于AF_INET6类型,这个值为16。

3 输入地址的类型,这个值为AF_INET或是AF_INET6。

在这里要注意,第一个参数为一个字符指针,实质上允许接受多种格式的地址。我们需要将我们的地址指针转换为(char *)来满足编译。第二个参数指明了所提供的地址的长度。

第三个参数为所传递的地址的类型。对IPv4的网络为AF_INET,也许在将来,这个值将会是IPv6地址格式的AF_INET6。

下面的例子程序是前一章的所演示的服务器程序的修改版本。这个服务器会在当前目录打开一个名为srvr2.log的日志文件,并且记录每一个连接。这个服务会同时记录IP地址以及主机名。

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#include <errno.h>

#include <time.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <netdb.h>

static void bail(const char *on_what)

{

if(errno != 0)

{

fputs(strerror(errno),stderr);

fputs(": ",stderr);

}

fputs(on_what,stderr);

fputc('/n',stderr);

exit(1);

}

int main(int argc,char **argv)

{

int z;

char *srvr_addr = NULL;

char *srvr_port = "9099";

struct sockaddr_in adr_srvr;

struct sockaddr_in adr_clnt;

int len_inet;

int s;

int c;

int n;

time_t td;

char dtbuf[128];

FILE *logf;

struct hostent *hp;



if(!(logf = fopen("srvr2.log","w")))

bail("fopen(3)");





if(argc>=2)

{



srvr_addr = argv[1];

}

else

{



srvr_addr = "127.0.0.1";

}



if(argc>=3)

srvr_port = argv[2];



s = socket(PF_INET,SOCK_STREAM,0);

if(s==-1)

bail("socket()");



memset(&adr_srvr,0,sizeof adr_srvr);

adr_srvr.sin_family = AF_INET;

adr_srvr.sin_port = htons(atoi(srvr_port));

if(strcmp(srvr_addr,"*") != 0)

{



adr_srvr.sin_addr.s_addr =

inet_addr(srvr_addr);

if(adr_srvr.sin_addr.s_addr == INADDR_NONE)

bail("bad address.");

}

else

{



adr_srvr.sin_addr.s_addr = INADDR_ANY;

}



len_inet = sizeof adr_srvr;

z = bind(s,(struct sockaddr *)&adr_srvr,len_inet);

if(z==-1)

bail("bind(2)");



z = listen(s,10);

if(z==-1)

bail("listen(2)");



for(;;)

{



len_inet = sizeof adr_clnt;

c = accept(s,(struct sockaddr *)&adr_clnt,&len_inet);

if(c==-1)

bail("accept(2)");



fprintf(logf,

"Client %s:",

inet_ntoa(adr_clnt.sin_addr));

hp = gethostbyaddr((char *)&adr_clnt.sin_addr,

sizeof adr_clnt.sin_addr,

adr_clnt.sin_family);

if(!hp)

fprintf(logf,"Error:%s/n",

hstrerror(h_errno));

else

fprintf(logf,"%s/n",

hp->h_name);

fflush(logf);



time(&td);

n = (int)strftime(dtbuf,sizeof dtbuf,

"%A %b %d %H:%M:%S %Y/n",

localtime(&td));



z = write(c,dtbuf,n);

if(z==-1)

bail("write(2)");



close(c);

}

return 0;

}

使用sethostent(3)函数

sethostent函数可以允许作为程序设计者的我们控制如果何执行名字服务器查询。这个函数可以改善我们程序的网络性能。这个函数的概要如下:

#include <netdb.h>

void sethostent(int stayopen);

sethostent函数只有一个输入参数。参数stayopen是一个布尔值输入参数。

当为真(非零)时,使用TCP/IP套接口执行查询,并且保持名字服务器处于打开状态。

当为假(零)时,使用UDP数据报进行名字服务器查询。

当我们的程序要执行频繁的名字服务器请求时,第一种情况是很有用的。对于许多查询来说,这是一种高性能的选择。然而,如果我们的程序只是在启动时执行一次查询,设置为FALSE是比较合适的,因为UDP有较少的网络负担。

在前面的例子中我们显示了如何使用gethostbyname函数来执行名字服务器查询。要使得这个程序使用连接的TCP套接口而不是UDP数据报,我们可以在程序中添加一个sethostent函数调用。

使用endhostent(3)函数

在使用TRUE调用sethostent函数之后,我们的程序就会进入一个处理的过程中,而程序却知道不再需要额外的名字查询。为了以一种节省的方式使用资源,我们需要一个方法来结束与名字服务器的连接,从而可以释放正在使用的TCP/IP套接口。这就是endhostent函数的目的。这个函数的概要如下:

#include <netdb.h>

void endhostent(void);

正如我们所看到的,这个函数没有参数,也没有返回值。

endhostent函数对于服务器,尤其是Web服务器而言是十分重要的,因为文件描述符是有限的。我们也许会记起一个套接口使用一个文件描述符,而每一个连接的客户端需要一个套接口。服务器的性能通常受到服务器所打开的文件描述符的数量的限制。这样,当服务器不再需要文件描述符时,关闭这些文件描述符就是十分重要的。

————————————————————————————————————————————————————————————————————————————
————————————————————————————————————————————————————————————————————————————
————————————————————————————————————————————————————————————————————————————
————————————————————————————————————————————————————————————————————————————

getaddrinfo()函数详解



1. 概述

IPv4中使用gethostbyname()函数完成主机名到地址解析,这个函数仅仅支持IPv4,且不允许调用者指定所需地址类型的任何信息,返回的结构只包含了用于存储IPv4地址的空间。IPv6中引入了getaddrinfo()的新API,它是协议无关的,既可用于IPv4也可用于IPv6。getaddrinfo函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个addrinfo的结构(列表)指针而不是一个地址清单。这些addrinfo结构随后可由套接口函数直接使用。如此以来,getaddrinfo函数把协议相关性安全隐藏在这个库函数内部。应用程序只要处理由getaddrinfo函数填写的套接口地址结构。该函数在
POSIX规范中定义了。

2. 函数说明

包含头文件

#include<netdb.h>

函数原型

int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result );

参数说明

hostname:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串)

service:服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftp、http等

hints:可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:如果指定的服务既支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。

result:本函数通过result指针参数返回一个指向addrinfo结构体链表的指针。

返回值:0——成功,非0——出错

3. 参数设置

在getaddrinfo函数之前通常需要对以下6个参数进行以下设置:nodename、servname、hints的ai_flags、ai_family、ai_socktype、ai_protocol。

在6项参数中,对函数影响最大的是nodename,sername和hints.ai_flag,而ai_family只是有地址为v4地址或v6地址的区别。ai_protocol一般是为0不作改动。

getaddrinfo在实际使用中的几种常用参数设置

一般情况下,client/server编程中,server端调用bind(如果面向连接的还需要listen),client则不用掉bind函数,解析地址后直接connect(面向连接)或直接发送数据(无连接)。因此,比较常见的情况有

(1) 通常服务器端在调用getaddrinfo之前,ai_flags设置AI_PASSIVE,用于bind;主机名nodename通常会设置为NULL,返回通配地址[::]。

(2) 客户端调用getaddrinfo时,ai_flags一般不设置AI_PASSIVE,但是主机名nodename和服务名servname(更愿意称之为端口)则应该不为空。

(3) 当然,即使不设置AI_PASSIVE,取出的地址也并非不可以被bind,很多程序中ai_flags直接设置为0,即3个标志位都不设置,这种情况下只要hostname和servname设置的没有问题就可以正确bind。

上述情况只是简单的client/server中的使用,但实际在使用getaddrinfo和参考国外开源代码的时候,曾遇到一些将servname(即端口)设为NULL的情况(当然,此时nodename必不为NULL,否则调用getaddrinfo会报错)。

以下分情况进行了测试:

(1) 如果nodename是字符串型的IPv6地址,bind的时候会分配临时端口;

(2) 如果nodename是本机名,servname为NULL,则根据操作系统的不同略有不同,本文仅在WinXP和Win2003上作了测试。

a) WinXP系统(SP2)返回loopback地址[::1]

b) Win2003则将本机的所有IPv6地址列表加以返回。因为通常一台IPv6主机都有可能不止一个IPv6地址,比如fe80::1(本机 loopback地址)、fe80::***的Link-Local地址、3ffe:***的全局地址等等。这种情况下调用getaddrinfo会将这些地址全部返回,调用者应该注意如何使用这些地址。另外要注意的是,对于fe80::的地址在绑定的时候必须标明接口地址,即使用 fe80::20d:60ff:fe78:51c2%4或fe80::1%1这样的地址格式,通过getaddrinfo直接取出fe80地址好像无法直接bind。





4. 使用细节

如果本函数返回成功,那么由result参数指向的变量已被填入一个指针,它指向的是由其中的ai_next成员串联起来的addrinfo结构链表。可以导致返回多个addrinfo结构的情形有以下2个:

1. 如果与hostname参数关联的地址有多个,那么适用于所请求地址簇的每个地址都返回一个对应的结构。

2. 如果service参数指定的服务支持多个套接口类型,那么每个套接口类型都可能返回一个对应的结构,具体取决于hints结构的ai_socktype成员。

我们必须先分配一个hints结构,把它清零后填写需要的字段,再调用getaddrinfo,然后遍历一个链表逐个尝试每个返回地址。



getaddrinfo解决了把主机名和服务名转换成套接口地址结构的问题。

其中,如果getaddrinfo出错,那么返回一个非0的错误值。

#include<netdb.h>

const char *gai_strerror( int error );

该函数以getaddrinfo返回的非0错误值的名字和含义为他的唯一参数,返回一个指向对应的出错信息串的指针。

由getaddrinfo返回的所有存储空间都是动态获取的,这些存储空间必须通过调用freeaddrinfo返回给系统。

#include< netdb.h >

void freeaddrinfo( struct addrinfo *ai );

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



5. 例子
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐