Linux Socket 学习(九)

主机名与网络名查询

在这一章,我们将会了解以下内容:
如何确定我们的本地主机名
如何将主机名解析为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> /* defines the following structure */
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中返回的信息。
/*uname.c
 *
 * Example of uname(2):
 */
#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函数

下面这个程序演示了这两个函数的用法。这个程序只是简单的调用这两个函数并报告其结果。
/*gethostn.c
 *
 * Example of gethostname(2):
 */
#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;     /* 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 */
};
/* for backward compatibility */
#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的用法。这个程序会在命令行接收多个主机名,然后分别查询每一个。所有可用的信息都会发送到标准输出,如果名字没有解析将会报告错误。
/*
 * lookup.c
 *
 * Example of gethostbyname(3):
 */
#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)
    {
    /*
     * Look up the hostname:
     */
    hp = gethostbyname(argv[x]);
    if(!hp)
    {
        /* Report lookup failure */
        fprintf(stderr,"%s:host '%s'/n",
            hstrerror(h_errno),argv[x]);
        continue;
    }

    /*
     * Report the findings:
     */
    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> /* for AF_INET */
struct hostent *gethostbyaddr(
        const char *addr, /* Input address */
        int len,          /* Address length */
    int type);        /* Address 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地址以及主机名。
/*srvr2.c
 *
 * Example daytime server,
 * with gethostbyaddr(3):
 */
#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>

/*
 * This function report the error and
 * exits back to the shell:
 */
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;    /* AF_INET */
    struct sockaddr_in adr_clnt;    /* AF_INET */
    int len_inet;            /* length */
    int s;                /* Socket */
    int c;                /* Client socket */
    int n;                /* bytes */
    time_t td;                /* current date&time */
    char dtbuf[128];            /* Date/Time info */
    FILE *logf;                /* Log file for the server */
    struct hostent *hp;            /* Host entry ptr */

    /*
     * Open the log file:
     */
    if(!(logf = fopen("srvr2.log","w")))
        bail("fopen(3)");
    
    /*
     * Use a server adderss from the command
     * line,if one has been provided.
     * Otherwise,this program will default
     * to using the arbitrary address
     * 127.0.0.1:
     */
    if(argc>=2)
    {
    /* Addr on cmdline: */
    srvr_addr = argv[1];
    }
    else
    {
    /* Use default address :*/
    srvr_addr = "127.0.0.1";
    }

    /*
     * If there is a second argument on the
     * command line,use it as the port #:
     */
    if(argc>=3)
    srvr_port = argv[2];

    /*
     * Create a TCP/IP socket to use:
     */
    s = socket(PF_INET,SOCK_STREAM,0);
    if(s==-1)
    bail("socket()");

    /*
     * Create a server socket address:
     */
    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)
    {
    /* Normal Address */
    adr_srvr.sin_addr.s_addr =
        inet_addr(srvr_addr);
    if(adr_srvr.sin_addr.s_addr == INADDR_NONE)
        bail("bad address.");
    }
    else
    {
    /* wild address */
    adr_srvr.sin_addr.s_addr = INADDR_ANY;
    }

    /*
     * bind the server address:
     */
    len_inet = sizeof adr_srvr;
    z = bind(s,(struct sockaddr *)&adr_srvr,len_inet);
    if(z==-1)
    bail("bind(2)");

    /*
     * Make it a listening socket:
     */
    z = listen(s,10);
    if(z==-1)
    bail("listen(2)");

    /*
     * Start the server loop:
     */
    for(;;)
    {
    /*
     * wailt for a connect:
     */
    len_inet = sizeof adr_clnt;
    c = accept(s,(struct sockaddr *)&adr_clnt,&len_inet);
    if(c==-1)
        bail("accept(2)");

    /*
     * log the address of the client
     * who connected to us:
     */
    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);

    /*
     * Generate a time stamp:
     */
    time(&td);
    n = (int)strftime(dtbuf,sizeof dtbuf,
        "%A %b %d %H:%M:%S %Y/n",
        localtime(&td));

    /*
     * Write result back the client:
     */
    z = write(c,dtbuf,n);
    if(z==-1)
        bail("write(2)");

    /*
     * Close this client's connection:
     */
    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服务器而言是十分重要的,因为文件描述符是有限的。我们也许会记起一个套接口使用一个文件描述符,而每一个连接的客户端需要一个套接口。服务器的性能通常受到服务器所打开的文件描述符的数量的限制。这样,当服务器不再需要文件描述符时,关闭这些文件描述符就是十分重要的。
posted @ 2007-10-08 22:49  jlins  阅读(556)  评论(0编辑  收藏  举报