第59章 SOCKET: Internet Domain

  在58章中提到过Internet domain socket地址由一个IP地址和一个端口号组成。虽然计算机使用了IP地址和端口号地二进制表示形式,但人们对名称地处理能力要比对数字地处理能力强的多。因此本章将介绍用名称标识计算机和端口地技术。此外,还将介绍如何使用库函数来获取特定主机名地IP地址与特定服务名对应地端口号,其中对主机名地讨论还包括了对域名系统(DNS)的描述,域名系统是一个分布式数据库,他将主机名映射到IP地址以及将IP地址映射到主机名。

59.1 Internet domain socket

  Internet domain 流socket是基于TCP之上的,他们提供了可靠的双向字节流通信信道。

  Internet domain 数据报socket是基于UDP之上的。UDPsocket与之在UNIX domain中的对应实体类似,但需要对应一下差别。

  • UNIX domain 数据报socket是可靠的,但是UDP socket是不可靠的----数据报可能会丢失、重复或到达的顺序与它们被发送的顺序不同。
  • 在一个UNIX domain数据报socket上发送数据会在接收socket的数据队列为满时阻塞。与之不同的是,使用UDP时如果进入的数据会使接收者的队列溢出,那么数据报2就会静默的被丢弃。

59.2 网络字节序

    IP地址和端口号是整数值。在将这些值在网络中传递时碰到的一个问题是不同的硬件结构会不同的顺序来存储一个多字节整数的字节。从图59-1中可以看出,存储整数时先存储(即在最小内存地址处)最高有效位的被称为大端,哪些最先存储最低有效位的被称为小端。小端架构最值得关注的时X86。其他大多数都是大端的。一些硬件结构可以在这两种格式之间切换。在特定主机上使用的字节序称为主机字节序。

  

图59-1:2字节整数和4字节整数的大端和小段字节序

   由于端口号和IP地址必须在网络中的所有主机之间传递并且需要被它们理解,因此必须要使用一个标准的字节序。这种字节序被称为网络字节序,他是大端的

  在本章后面会介绍各种用于将主机名(如www.kernel.org)和服务名(如http)转换成对应的数字形式的函数。这些函数一般会返回用网络字节序表示的整数,并且可以直接将这些整数复制进一个socket地址结构的相关字段中。

  有时候可能会直接使用IP地址和端口号的整数常量形式,如可能会选择将端口号硬编码进程序中,或者将端口号作为一个命令行参数传递给程序,或者在指定一个IPv4地址时使用注入INADDR_ANY和INADDR_LOOPBACK之类的常量。这些在C中是按照主机的规则来标识的,因此他们时主机字节序的,在他们存储进socket地址结构中之前需要将这些值转换成网络字节序。

  htons()、htonl()、ntohs()、以及ntohl()函数被定义(通常为宏)用来在主机字节序和网络字节序之间转换整数。

#include <arpa/inet.h>
uint16_t htons(uint16_t host_uint16);
                return host_uint16 converted to network byte order

uint32_t htonl(uint32_t host_uint32);
                return host_uint32 converted to network byte order

uint16_t ntohs(uint16_t net_uint16);
                return net_uint16 converted to host byte order

uint32_t ntohl(uint32_t net_uint32);
                return net_uint32 converted to hostbyte order

在早期,这些函数的原型如下。

unsigned long htonl(unsigned long hostlong);

这揭示了函数名的由来---在本例中是host to network long。在大多数实现socket的早期系统中,短整数是16位的,长整数是32位的。但是现在系统中这种论断已经不再正确了。(至少是对于长整数是这样的)因此上面给出的原型实际上是为这些函数所处理的汉纳树提供了个更加精确的定义,尽管所使用的函数名称未发生变化。uint16_t和uint32_t数据类型是16位和32位的无符号整数。

  严格的讲,只需要在主机字节序与网络字节序不同的系统上使用这4个函数,但开发人员应该总是使用这些函数,这样程序就能够在不同的硬件结构之间移植了。在主机字节序与网络字节序一致的系统上,这些函数只是简单的原样返回传递给他们的参数。

59.3 数据表示

  在编写网络程序时需要清楚不同的计算机架构使用不同的郭泽来表示各种数据类型。本章之前已经指出过整数类型可以大端或者小端进行存储。此外还存在其他的差别,如C long数据类型 在一些系统中可能是32位的,但在其他系统中可能是64位的。当考虑结构时,问题就更复杂了,因为不同的实现采用了不同的饿规则来讲结构体中的一个字段对齐到主机系统的地址边界,从而使得字段之间的填充字节数量是不同的。

  由于数据表现上存在差异,因此在网络中的异构系统之间交换数据的应用程序必须要采用一些一些公共规则来编码数据。发送者必须要根据这些规则来对数据进行编码,而接收者必须要遵循同样的郭泽对数据解码。将数据变成一个标准格式以便在网络上传输的过程被称为信号编集。目前,存在多种信号编集标准,如XDR(ExterExternalData Repressentation,)、CORBA以及XML.一般来讲,这些标准会为每一种数据类型都定义一个固定的格式(如顶一个字节序和使用的位数)。除了按照所需的格式进行编码之外,每一个数据项都需要使用额外的字段来标识其类型(以及可能的话还会加上其长度)。

  然而,一种比信号集更简单的方法通常会被采用:所有传输的数据编码成文本形式,其中数据项之间使用特定的字符来分开,这个特定的字符就是换行符。这个方法的一个优点就是可以使用telnet来调试一个应用程序,要完成这项任务需要使用下面的命令。

$ telnet host port

  接着可以输入一行传给应用程序的文本并查看应用程序发来的响应,在59.11节会演示这项技术

与异构系统在数据表示上的差异相关的问题不仅仅存在于网络见的数据传输中,还存在于此类系统之间的任何数据交换机制中,如在传输异构系统见磁盘或者磁带上的文件时会遇到同样的问题。现在网络编程只不过是可能会遇到这类问题的最常见的编程场景

如果将在一个流socket上传输的数据编码成用换行符分隔的文本,那么定义一个诸如redLine()之类的函数将是比较便捷的,如程序清单59-1所示。

#include "read_line.h"

ssize_t readLine(int fd,void *buffer,size_t n);
  Returns number of bytes copird into buffer(excluding terminating
 null byte),or 0 on end of file.or -1 on error

 readLine()函数从文件描述符参数fd引用的文件读取字节知道碰到换行符为止。输入字节序列将会返回在buffer指向的为止处,其中buffer指向的内存区域至少为n字节。返回的字符串总是以null结尾,因此实际上至多有(n-1)个字节返回,在成功时,readLine()会返回放入buffer的数据的字节数,结尾的null字节不会计算在内。

#include <unstd.h>
#include <errno.h>
#include "read_line.h"  //declaration of readline()
ssize_t readLine(int fd,void *buffer,size_t n)
{
    ssize_t numRead;// # of bytes fetched by last read()
    size_t totRead ;     //total bytes read to far
    char * buf;
    char ch; 
    if(n <0 || buffer == NULL)
    {
        errno = EINVAL;
        return -1;
    }

    buf = buffer;  //No pointer arithmetic on "void *"
    totRead = 0;
    for(;;)
    {
        numRead = read(fd,&ch,1);
        if(numRead == -1)
        {
            if(errno == EINTR) //interrupted ->restart read
                continue;
            else
                return -1;
        }else if(numRead == 0)//EOF
        {
            if(totRead == 0)  //No bytes read; return 0
                return 0;
            else
                break;      //some bytes read add '\0'
        }else{
            if(totRead <n-1){ // numRead must be 1 if we get here
                totRead++;    // discard > (n-1) bytes
                *buf ++ = ch;
            }
            if(ch == '\n')
                break;
        }
    }

    *buf = '\0';
    return totRead;
}

  如果在遇到换行符之前读取的字节数大于或等于(n-1),那么readLine()函数胡丢弃多余的字节(包括换行符)。如果在前面的(n-1)字节中读取了换行符,那么再返回的字符串中就会包括这个换行符。(因此可以通过检查返回的buffer中结尾null字节前是否是一个换行符来确定是否有字节被丢弃了。)采用这种方法之后,将输入以行为单位进行处理的应用程序协议就不会将一个很长的行处理成多行了。当然这可能会破坏协议,因为两端的应用程序不再同步了。另一种做法是让readLine()只读取足够的字节数来填充提供的缓冲器,而将到下一行新行为止的剩余字节留给下一个readLine调用。在这种情况下,readLine()的调用者需要处理读取部分行的情况。

59.4 Internet socket 地址

  Internet domain socket地址有两种:IPv4和IPv6。

IPv4 socket地址:struct sockadr_in

  一个IPv4 socket 地址会被存储在一个sockaddr_in 结构体中,鬼结构体在<netinet/in.h>中进行定义,具体如下。

struct in_addr{   //IPv4 4-byte addr
    in_addr_t s_addr;    //unsigned 32-bit integer
};

struct sockaddr_in{//IPv4 socket address
    sa_faminly_t    sin_family;   //Adess family (AF_INET)
    in_port_t       sin__port;    //port number
    struct in_addr  sin_addr;    //IPv4 address
    unsigned        char _pad[X];  //Pad to sizeof 'sockaddr' structure 16 bytes

}

  56.4节中曾讲到过普通的sockaddr结构中由一个字段来表示socket domain,该字段对应于sockaddr_in 结构体中的sin_family字段,其值总为AF_INET。sin_port和sin_addr字段是端口号和ip地址,他们都是网络字节序的。in_port_t和in_addr_t数据类型是无符号整型,其长度分别为16位和32位。

IPv6 socket地址:struct sockaddr_in6

  与IPv4一样,一个IPv6socket地址包含一个IP地址和一个端口号,他们之间的差别在于IPv6地址是128位。一个IPv6地址会被存储在一个sockaddr_in6结构中,该结构在<netinet/in.h>中进行定义,具体如下。

struct in6_addr{            //IPv6 address structure
    uint8_t s6_addr[16];     // 16 bytes = 128bits
};

struct sockaddr_in6{      //IPv6 socket address
    sa_family_t        sin6_family;        //Adderss family(AF_INET6)
    in_port            sin_port;           //port number
    uint32_t           sin6_flowinfo;      //IPv6 fow information
    struct in6_addr    sin6_addr;          //IPv6 address
    uint32_t           sin6_scope_id;      // Scope ID(new in kernel 2.4)
};

         sin_family字段会被设置成AF_INET6。sin6_port和sin_addr字段分别时端口号和IP地址。(uint8_t数据类型被用来定义in6_addr结构体中字节的类型,他是一个8位的无符号整型。)剩余的字段sin6_flowinfo和sin6_scope_id则超出了本书的范围。在本书中设置的所有例子中都将他们设置为0.sockaddr_in6结构中的所有字段都是以网络字节序存储的。

        IPv6和IPv4一样有通配和会还地址,但他们的用法要更加复杂一些,因为IPv6地址是存储在数组中的(并没有使用标量类型),下面将会用IPv6通配地址(0:0)来说明这一点。系统定义了常量IN6ADDR_ANY_INIT来表示这个地址,具体如下:

#define IN6ADDR_ANY_INIT = {{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}};

在Linux上头文件中的一些细节与本节中的描述是不同的,特别地,in6_addr结构包含了一个union定义将128位的IPv6地址划分成16字节或八个2字节的整数或四个32字节的整数,由于存在这样的定义,因此glibc提供的IN6ADDR_ANY_INT常量的定义实际比正文中给出的防疫多了一组嵌套的花括号

  在变量声明的初始化器中可以使用IN6ADDR_ANY_INIT常量。但无法在一个赋值语句的右边使用这个常量,因为C语法并不允许在赋值语句中使用一个结构化的常量。取而代之的做法是必须要使用一个预先定义的变量in6addr_any,C库会按照下面的方式对该变量进行初始化。

const struct in6_addr in6addr_any = IN6ADDR_ANY_INIT

//因此可以像下面这样使用通配地址

struct sockaddr_in6 addr;

memset(&addr,0,sizeof(struct sockaddr_in6));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons(SOM_PORT_NUM);  /

        IPv6换回地址(::1)的对应常量和变量是IN6ADDR_LOOPBACK_INIT和in6addr_loopback。

        与IPv4中相对应字段不同的是IPv6的常量和变量初始化器是网络字节序的,但就像上面给出的代码那样,开发人员仍然必须要确保端口号时网络字节序的。

        如果IPv4和IPv6共存于一台主机上,那么他们将共享同一个端口空间。这意味着如果一个应用程序将一个IPv6 socket 绑定到了TCP端口2000上(使用IPv6通配地址),那么IPv4 TCP socket将无法绑定到同一个端口上。(TCP/IP实现确保位于其他主机上的socket能够与这个socket通信,不管那些主机运行的时IPv4还是IPv6).

sockaddr_stroage结构

        在IPv6 socket API中新引入了一个通用的sockaddr_stroage结构,这个结构的空间足以存储任何类型的socket地址(即可以讲任何类型的socket 地址强制转换并存储在这个结构中)。特别 的,这个结构允许透明地存储IPv4 和IPv6socket 地址,从而删除了代码中的IP版本依赖性。sockaddr_storage 结构在Linux上的定义如下。

#define __ss_aligntype uint32_t         //on 32bit architectures
struct sockaddr_storege{
    sa_family_t ss_family;
    __ss_aligntype __ss_align;      //force alignment
   char __ss_padding[SS_PADSIZE];   //Pad to 128 bytes
}

59.5 主机和服务转换函数概述

         计算机以二进制形式来表示IP地址和端口号,但人们发现名字比数字更容易记忆,使用符号还能有效的利用间接关系,用户和程序可以继续使用同一个名字,即使底层的数字值发生了变化也不会收到影响。

        主机名和连接在网络上的一个系统(可能拥有多个IP地址)的符号标识符。服务名时端口号的符号表示。

        主机地址和端口的表示有下列两种方法。

  •         主机地址可以表示为一个二进制值或者一个符号主机名或展现格式(IPv4是点分十进制,IPv6是一个十六进制字符串)。
  • 端口号可以表示为一个二进制值或一个符号服务名。

        格式之间的转换可以通过各种库函数来完成,本节将对这些库函数进行简要的小结,下面几个小结将会详细描述现代API(inet_ntop()、inet_pton()、getaddrinfo()、getnameinfo()等)。在59.13节或简要的介绍一下被废弃的API(inet_aton(),inet_ntoa(),gethostbyname()、getserverbyname()等)。

    在二进制和人类之间刻度的形式之间转换IPv4地址

        inet_aton()和 inet_ntoa()函数时间一个IPv4地址在点分十进制和二进制表示性质之间进行转换。这里介绍这些函数主要原因是读者在遗留代码中可能会看到这些函数。现在他们已经被废弃了,需要完成此类转换工作的现代程序应该使用接下来而描述的函数。

        在二进制和人类可读的形式之间转换IPv4和IPv6地址

         inet_pton(0和inet_ntop()与inet_aton()和inet_ntoa()类似,但他们还能处理IPv6地址。他们将二进制IPv4地址和IPv6地址转换成展现格式---即点分十进制或十六进制字符串表示,或将展现格式转换层IPv4和IPv6地址。

        由于人类对名字的处理能力要比数字要强,因此通常会在程序中 使用这些函数。ine_ntop()的一个用途是产生IP地址的一个可打印的表示形式以便记录日志。有些情况下,最好使用这个函数而不是将一个IP地址转换(“解析”)成主机名,其原因如下。

  •         将一个IP地址解析成主机名可能需要向一台DNS服务器发送一个耗时较长的请求。
  • 在一些场景中,可能并不存在一个DNS(PTR)记录将IP地址映射到对应的主机名上。

本章在介绍二进制表示与对应的符号名之间的转换的工作的getaddrinfo()和getnameinfo之前(59.6节)先介绍这些函数的主要原因时因为他们能提供更贱的API,这样就能够快速给出一些正常使用的Inetrnet domain socket的例子

        主机和服务名与二进制形式之间的转换(已过时)

         gethostbyname()返回与主机名对应的二进制IP地址,getserverbyname()函数返回与服务名对应的端口号。对应的逆向转换是由gethostbyaddr()和getserverbyport()来完成的。这里之所以要介绍这些函数是因为他们在既有代码中被广泛使用,但现在他们已经过时了。(SUSv3标记这些函数为过时的,SUSv4删除了他们的规范。)新代码应该使用getaddrinfo()和getnameinfo()来完成此类转换

主机名和服务名与二进制之间的转换(现代的)

        getaddrinfo(0函数时gethostbyname()和getserverbyname()两个函数的现代继任者。给定一个主机名和服务名,getaddrinfo()会返回一组包含对应二进制IP地址和端口号的结构。与gethostbyname()不同,getaddrinfo()会透明的处理IPv4和IPv6地址。因此使用这个函数可以编写不依赖于IP版本的程序。所有的新代码都应该使用getaddrinfo()来将主机名和服务名转换成二进制表示。

        getnameinfo()函数进行逆向转换,即将一个IP地址和端口号转换成对应的主机名和服务名。

        使用getaddrinfo()和getnameinfo()还可以在二进制IP地址与其展现格式之间进行转换

    DNS允许写作服务器维护一个将二进制IP地址映射到主机名和将主机名映射到二进制IP地址的分布式数据库。诸如DNS之类的系统的存在低于internet的运转是非常关键的,因为对浩瀚的因特网主机名进行集中管理是不可能的。/etc/services 文件将端口映射到符号服务名。

59.6 inet_pton()和inet_ntop()函数

        inet_pton()和inet_ntop()函数允许在IPv4和IPv6地址的二进制形式和点分十进制表示法或十六进制字符串表示法之间进行转换。

#include <arpa/inet.h>
inet_pton(int domain,const char *str_str,void *addrptr)
        Return 1 onsuccessful vonversion, 0 on error;
const char *inet_ntop(int domain,const void *adrptr,char *dst_str,size_t len);
        Return pointer to dst_str onsuccess,or NULL on error

 这些函数名中的p表示“展现(presentation)”,n表示“网络(network)”。展现形式是人类可读的字符串,如:

  • 204.152.189.116(IPv4点分十进制地址)
  • ::1(IPv6冒号分割的16进制地址)
  • ::FFFF:204.152.189.116(IPv4映射的IPv6地址)。

inet_pton()函数将ssrc_str中包含的展现字符串转换成网络字节序的二进制IP地址。domain参数应该被指定为AF_INET或AF_INET6。转换得到的地址会被放在addrptr指向的结构中,他应该根据在domain参数中指定的值指向一个in_addr或in6_addr结构。

        inet_ntop()函数执行逆向转换。同样,domain应该被指定为AF_INET或者AF_INET6,addrptr应该指向一个待转换的in_addr或in6_addr结构。得到的以null结尾的字符串会在成功时会返回dst_str.如果len太小了,那么inet_ntop()会返回NULL并将errno设置为ENOSPC.

        要正确计算dst_str指向的缓冲区的大小可以使用<netinet/in.h>中定义的两个常量,这些常量标识出了IPv4和IPv6地址的战象字符串的最大长度(包括结尾的null字节)

#define INET_ADDRSTRLEN    16  //Maxnum IPv4 sotted-decimal string
#define INET6_ADDRSTRLEN   46  //Maxnum IPv6 hexadecimal string

59.7 客户端/服务器示例(数据报socket)

        程序请59-3 给出了服务器程序,服务器使用inet_ntop函数将客户端的主机地址(通过fecvfrom调用获得)转换成可打印格式。 

$ ./i6d_ucase_sv &
[1] 31047

$ ./i6d_ucase_cl ::1 clao
Server received 4 bytes from (::1,32770)
Response 1 CIAO

        程序清单59-4给出的客户端程序与之前的UNIX domain 的版本(程序清单57-7)相比存在两个显著的改动。第一个差别在于客户端会将其第一个命令含参数解释成服务器的IPv6地址。(剩余的命令行参数是作为单独的数据报被传给服务器的。)客户端使用inet_pton()将服务器地址转换成二进制形式。另一个差别在于客户端并没有将其socket绑定到一个地址上。在58.6.1节中支出过如果一个Internet domain socket没有被绑定到一个地址上,那么内核会将该socket绑定到主机系统上的一个临时端口上。这一点可以从下面的shell回话日志中看出,其中客户端和服务器运行在同一个主机上。

    从上面的输出中可以看出recvfrom()调用能够获取kehuduansocket的地址,包括临时端口号,不管客户端是否调用额bind().

//程序清单59-3 使用数据包soclet的IPv6大小写转换服务器  i6d_ucase_v.c
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>

#define BUF_SIZE  10  /*Maxnum size of messages exchanged
                       between client and server*/
#define PORT_NUM 5002   //server port number

int main(int argc, char *argv[])
{
    struct sockaddr_in6 svaddr,claddr;
    int sfd ,j;
    ssize_t numBytes;
    socklen_t len;
    char buf[BUF_SIZE];
    char claddrStr[INET6_ADDRSTRLEN];

    sfd = socket(AF_INET6,SOCK_DGRAM,0);
    if(sfd == 1)
    {
        printf("socket");
        return -1;
    }
    memset(&svaddr,0,sizeof(struct sockaddr_in6));
    svaddr.sin6_family = AF_INET6;
    svaddr.sin6_addr = in6addr_any;
    svaddr.sin6_port = htons(PORT_NUM);

    if(bind(sfd,(struct sockaddr *)&svaddr,sizeof(struct sockaddr_in6)) == -1)
    {
        printf("bind");
        return -1;
    }

    /*Receive messages convert to uppercase ant return client*/
    for(;;)
    {
        len = sizeof(struct sockaddr_in6);
        numBytes = recvfrom(sfd,buf,BUF_SIZE,0,(struct sockaddr *)&claddr,&len);
        //printf("claddr.sin6_addr = %s\n",claddr.sin6_addr);
        if(numBytes == -1)
        {
            printf("recvrom\n");
            return -1;
        }
        if(inet_ntop(AF_INET6,&claddr.sin6_addr,claddrStr,INET_ADDRSTRLEN) == NULL)
            printf("Couldn't conver client address to string\n");
        else
            printf("Server reveived %ld bytes from (%s,%u)\n",
                    (long)numBytes,claddrStr,ntohs(claddr.sin6_port));
        
        for(j=0;j<numBytes;j++)
        {
            buf[j]=toupper((unsigned char) buf[j]);
        }

        if(sendto(sfd,buf, numBytes,0,(struct sockaddr *)&claddr,len)!=numBytes)
            perror("sendto ");
    }
}
// 使用数据报socket的IPv6大小写转换客户端        i6d_ucase_cl.c
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <ctype.h>
#include <string.h>
#include <stdio.h>

#define BUF_SIZE  10  /*Maxnum size of messages exchanged
                       between client and server*/
#define PORT_NUM 5002   //server port number

int main(int argc,char *argv[])
{
    struct sockaddr_in6 svaddr;
    int sfd, j;
    size_t msgLen;
    ssize_t numBytes;
    char resp[BUF_SIZE];

    if(argc <3 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s host-address msg ...\n",argv[0]);
    }
    sfd = socket(AF_INET6,SOCK_DGRAM,0);
    if(sfd == -1)
    {
        perror("socket");
        return -1;
    }
    memset(&svaddr,0,sizeof(struct sockaddr_in6));
    svaddr.sin6_family = AF_INET6;
    svaddr.sin6_port = htons(PORT_NUM);

    if(inet_pton(AF_INET6,argv[1],&svaddr.sin6_addr)<=0)
    {
        printf("inet_pton failed for address '%s'",argv[1]);
    }
    /*send message to server;echo responses on stdout*/
    for(j=2;j<argc;j++)
    {
        msgLen = strlen(argv[j]);
        if(sendto(sfd,argv[j],msgLen,0,(struct sockaddr *)&svaddr,
            sizeof(struct sockaddr_in6)) != msgLen)
        {
            printf("sendto");
        }
        numBytes = recvfrom(sfd,resp,BUF_SIZE,0,NULL,NULL);
        if(numBytes == -1)
        {
            perror("recvfrom");
            return -1;
        }
        printf("Response %d:%.*s\n",j-1,(int)numBytes,resp);
    }

    return 0;
}

59.8 DNS 域名系统

        在59.10节中将会介绍获取 获取与一个主机名对应的IP地址的getaddrinfo函数和逆向转换的getnameinfo()函数,但在介绍这些函数之前需要解释如何使用DNS来维护主机名和IP地址之间的映射关系。

        在DNS出现以前,主机名和IP地址之间的映射关系是在一个手工维护的本地文件/etc/hosts中定义的,该文件包含了如下的记录

# IP-address canonical host name      [aliases]

127.0.0.1 localhost

        gethostbyname()函数(被getaddrinfo()取代的函数)通过搜索这个文件并找出与规范主机名或其中一个别名(可选的,以空格分离)匹配的记录来获取一个IP地址。

        然而,/etc/hosts模式的扩展性较差,并且随着网络主机数量的增长(如因特网中存在着数以亿万记的主机)这种方式已经变得不大可行了。

        DNS被设计用来解决这个问题。DNS关键想法如下。

  •         将主机名组织在一个层级名名空间中(图59.2)DNS层级中的每一个节点都有一个标签(名字),该标签最多可包含63个字符。层级的根是一个无名子节点,即“匿名节点”。
  • 一个节点的域名由该节点到根节点的路径中所有节点的名字连接而成,各个名字之间用点(.)分隔,如google.com时节点google的域名。
  • 完全限定域名(fully qualified domain name,FQDN),如www.kernel.org.表示出了层级中的一台主机。区分一个完全限定域名的方法是看名字是否以点结尾,但在多数情况下这个点会被忽略,
  • 没有一个组织或者系统会管理整个层级。相反,存在一个DNS服务器层级,没他服务器管理树的一个分支(一个区域)。通常,每个区域都有一个主要名字服务器。此外,还包含一个或多个从名字服务器(有时候也被称为次要主名字服务器),他们在主要主名字服务器崩溃时提供备份。区域本身可以被花城一个个单独管理的更小的区域。当一台主机被添加到一个区域中或主机名到IP地址之间的映射关系发生变化时,管理员负责更新本地名字服务器上的名字数据中的对应名字。(无需手动更改层级中其他名字服务器数据库)。
  • 当一个程序调用getaddrinfo()来解析(即获得一个IP地址)一个域名时,getaddrinfo()会使用一组库函数(resolver库)来与本地的DNS服务器通信,如果这个服务器无法提供所需信息,那么他就会与位于层级中的其他DNS服务器进行通信以便获取信息,有时候这个过程肯呢会话费很多时间,DNS服务器采用了缓存技术来避免在查询常见域名时,所发生的不必要的通信。

使用上面的方法使得DNS能够处理大规模命名空间,同时无需对名字进行集中处理。

递归和迭代的解析请求

        DNS解析请求可以分为两类:递归和迭代。在一个递归请求中,请求者要求服务器处理整个解析任务,包括在必要的时候与其他DNS服务器进行通信的任务。当位于本地主机上的一个应用程序调用getaddrinfo()时,该函数回想本地DNS服务器发送一个递归请求,如果本地DNS服务器自己并没有相关信息来完成解析,那么他就会迭代的解析这个任务。

图59-2:DNS层级的一个子集标题

        下面通过一个例子来解释迭代解析,结社本地DNS服务器需要解析一个名字www.otago.ac.nz。要完成这个任务,他首先与每个DNS服务器都知道的以小组更名字服务器中的一个进行动心。(使用命令dig.NS或从网页http://www.root-server.org/上可以获取这组服务器列表。)给定名字www.otago.ac.nz,根名字服务器会告诉本DNS服务器到其中一台nzDNS服务器上查询。然后本地DNS服务器会在nz服务器上查询名字www.otago.ac.nz,并收到一个到ac.nz服务器上查询的响应。之后本地DNS服务器会在ac.nz服务器上查询www.otago.ac.nz并被告知查询otago.ac.nz服务器。最后DNS服务器会在otago.ac.nz拂去其上查询www.otago.ac.nz并获取所需的IP地址。

        如果向gethostbyname()传达了一个不完整的域名,那么解析器在解析之前会尝试补全。域名补全的规则是在/etc/resolv.conf中定义的(参见resolve.conf(5)手册)。在默认情况下,解析器至少会使用本机的域名来补全。例如,如果登录机器oghma.otago.ac.nz并输入了命令 ssh octavo,得到的DNS查询将会议octavo.otago.ac.nz做为其名字

顶级域

        紧跟在匿名根节点下面的节点被称为顶级域(TLD)。在这些节点之下的时二级域,以此类推。 TLD可以分为两类,通用的和国家的。

        在历史上存在七个通用的TLD,其中大多数都可以被看成是国际的。在59-2中给出了其中4个原始通用的TLD。另外三个时int、mil和gov,其中后两个是保留给美国使用的。进来一组新的通用TLD被添加进来了(如info、name和museum)。

        每个国家都有一个对应的国家(或地理)TLD(在ISO3166-1中进行了标准化),他是一个由2个字符组成的名字在图59-2给出了其中一些:de(德国,deutschland)、eu(欧洲联盟国家吵过架地理TLD),nz(新西兰)以及us(美利坚合众国)。一些国家将他们的TLD或分成一组二级域名,其划分方式与通用语类似。如新西兰用ac.nz(学术机构),co.nz(商业)以及govt.nz(政府)。

59.9 /etc/services 文件

        正如58.6.1节中指出的那样,众所周知的端口号是由IANA集中注册的,其中每个端口都有一个对应的服务名。由于服务号时集中管理并且不会像IP地址那样频繁变化,因此没有必要采用DNS服务器来管理他们。相反,端口号和服务名会记录在文件/etc/servives中,getaddrinfo()和getnameinfo()函数会使用这个文件中的信息在服务名和端口号之间进行转换。

         协议通常时TCP或udp,可选的(以空格分隔)别名指定了服务的其他名字,此外,没一行中都可能会包含以#字符大头的注释。

正如之前指出那样,一个给定的端口号引用UDP和TCP的唯一实体,但IANA的策略试讲两个端口都分配给任务,及时服务只是用了其中一种协议。如telnet、ssh、http以及SMTP,他们都只使用TCP,但是对应的UDP端口也被分配给了这些服务。相应地,NTP只是用UDP,但TCP端口123也被分配给了这个任务。在一些情况中,一个服务既会使用TCP也会使用UDP,DNS和encho就是这样的服务,最后还有一些极少出现的情况将数值相同的UDP和TCP端口分配不同的服务,如rsh使用TCP端口514,而syslog daemon(37.5)则是使用了UDP端口514.这是因为这些端口在采用线性的IANA策略之前就分配出去了。

        /ETC/SERVICES文件仅仅记录着名字到数字的映射关系。它不是一种预留机制:在/etc/services中存在的一个端口号能保证在实际环境中特定的服务就能够绑定在该端口上。

59.10 独立于协议的主机和服务转换

        getaddrinfo()函数将主机和服务名转换成IP地址和端口号,他作为过时的gethostbyname()和getservbyname()函数的(可重入的)阶梯这被定义在了POSIX.1g中。(使用getaddrinfo()替代gethostbyname()能够从程序中删除IPv4和IPv6的依赖关系。)

        getnameinfo()函数是getaddrinfo()函数的逆向函数,他将一个socket地址结构(IPv4或IPv6)转换成对应主机和服务名的字符串。这个函数是过时的gethostbyaddr()和getservbyport()函数的(可重入的)的等价物。

59.10.2 getaddrinfo() 函数

        给定一个主机名和服务器名,getaddrinfo()函数返回一个socket地址结构列表,每个结构 都包含一个地址和端口号。

#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char*host,const char *servidce,
                const struct addrinfo *hints,struct addrinfo **result)
            Rerturn 0 on success, or nozero on error

        getaddrinfo()以host、service以及hints参数作为输入,其中host参数包含一个主机名或一个以IPv4点分十进制标记或IPv6十六进制字符串标记的数值地址字符串。(准确的讲,getaddrinfo()接受在59.13.1节中描述的更通用的数字和点标记的IPv4数值字符串。)service参数包含一个服务名和一个十进制端口号。hints参数指向一个addrinfo结构,该结构规定了选择通过results返回的socket地址结构的标准。

        getaddrinfo()会动态地分配给一个包含addrinfo()结构的链表并将result指向这个列表的表头。每个addrinfo结构包含一个指向与host和service对应的socket地址结构的指针(图59-3)。addrinfo结构的形式如下:

        

struct addrinfo{
    int     ai_flags;                //input flags(AI_ *constants)
    int     ai_family;               // address family;
    int     ai_socketype;            //Type:SOCK_STREAM,SOCK_DGRAM;
    int     ai_protoaol;             // Socket protocol
    size_t  ai_addrlen;              //Size of structure pointed to by ai_addr
    char *  ai_canonname;            // Canonical name of host
    struct sockaddr * di_addr         //Pointer to socket address structure
    struct addrinfo *ai_next           // next structure in linked list
}

        result参数返回一个结构列表而不是单个结构,因为在于host、service以及hints中指定的标准对应的主机和服务组合可能有多个。如查询拥有多个网络接口的主机可能会多个地质结构。此外,如果将hints.ai_socktype参数指定为0,那么可能就会返回两个结构,-----一个用于SOCK_DGRAM socket,l另一个用于SOCK_STREAM socket  ---前提是给定的service同时对TCP和UDP可用。

        通过result返回的addrinfo结构的字段描述了关联socket地址的属性。ai_family字段会被设置成AF_INT或AF_INET6,表示该socket地址结构的类型。ai_socktype字段会被设置成SOCK_DGRAM或SOCK_STREAM,表示这个地址时用于UDP服务还是TCP服务。ai_protocol字段会返回与地质组和socket类型匹配的协议值。(ai_family、ai_socktype以及ai_protocol三个字段为调用socket()创建改地址上的socket时所需的参数提供了取值。)ai_addrlen字段给出了ai_addr指向的socket地址结构的大小(字节数)。in_addr字段指向socket地址结构(IPv4时 是一个in_addr结构,IPv6时是一个in6_addr结构)。ai_flags字段未用(它用于hints参数),ai_canonname字段仅有第一个addrinfo结构使用并且其前提事项下面所描述的那样在hints.ai_flags中使用了AI_CANONNAME字段。

        与gethostbyname()一样,getaddrinfo(0可能需要向一台DNS服务器发送一个请求,并且这个请求可能还要发送一段时间来完成,同样的过程也适用于getnameinfo(),

图59-3:getaddrinfo分配和返回的结构

 hints参数

        hints参数为如何选择getaddrinfo()返回的socket地址结构指定了更多的标准。当用作hints参数时智能设置addrinfo结构的ai_flags、ai_family、ai_socktype以及ai_procotol字段,其他字段未用到,并将应该根据具体情况将其初始化为0或者NULL。

        hints.ai_family字段选择了返回的socket地址结构的域,其取值可以是AF_INET或AF_INET6(或其它一些AF_*常量,只要实现支持他们)。如果需要获取所有种类socket地址结构,那么可以将这个字段的值指定为AF_UNSPEC。

        hints.ai_cosktype字段指定了使用返回的socketd地址结构的socket类型。如果将这个字段指定为SOCK_DGRAM,那么查询会在UDP服务上执行,对应的socket地址结构会通过result返回,如果指定了SOCK_STRAM,那么将会执行一个TCP服务查询。如果将hints.ai_socktype指定为0,那么任意类型的socket都是可接受的。

        hints.ai_protocol字段为返回的地址结构选择了socket协议。在本书中这个字段的值总是会被设置为0,表示调用者接受任何协议。

        hints._ai_flags字段是一个位掩码,他会改变getaddrinfo()的行为。这个字段的取值是下列值中的零个或多个取OR得来的。 

AI_ADDRCONFIG

        在本地系统上至少配置了一个IPv4地址时返回IPv4地址(不是IPv4回环地址),IPv6地址也是如此。 

AI_CANONNAME

        如果host不为NULL,那么返回一个指向以null为结尾的字符串,该字符串包含了主机的规范名。这个指针会在通过result返回的第一个addrinfo结构中的ai_canonname字段指向的缓冲器中返回。

AI_NUMERICHOST

        强制将host解释成一个数值地址字符串。这个常量用于在不必要解析名字时防止进行名字解析,因为名字解析可能会话费较长的时间。

AI_NUMERICSERV

        将service解释成一个数值端口号,这个标记用于防止调用任意的名字解析服务,因为当service为一个数值字符串时这种调用是没有必要的。

AI_PASSIVE

        返回一个适合进行被动式打开(即一个监听socket)的地址结构。在这种情况下,host应该是NULL,通过result返回desocket地址结构的IP地址部分将会包含一个通配IP地址(即INADDR_ANY或IN6ADDR_ANY_INIT)。如果没有这个标记,那么通过result返回的地址结构将能用于connect()和sendto;如果HOST为NULL,那么返回的socket地址机构中的IP地址将会被设置成回环IP地址(根据所处的域,其值为INADDR_LOOPBACK或IN6ADDR_LOOPBACK_INIT).

AI_V4MAPPED

        如果在hints的ai_family字段中指定了AF_INET6,那么在没有找到匹配的IPV6地址时应该咋result返回IPv4映射的IPv6地址结构。如果同时指定了AI_ALL和AI_V4MAPPED,那么在result中会同时返回IPv6和IPv4地址,其中IPv4地址会被返回成IPv4地址映射的IPv6地址结构。

        正如前面su偶说的AI_PASSIVE时指出的那样,host可以被指定为NULL。此外还可以将service指定为NULL,在这种情况下,返回的地址结构中的端口号会被置为0(即只关心将主机名解析成地址)。然而无法将host和sevice同时指定为NULL。

        如果无需在hints中指定上述的选取标准,那么可以将hints指定weiN,在这种情况下会将ai_socktype和ai_protocol假设为0,将ai_flags假设为(AI_V4MAPPED | AI_ADDRCONFIG),将ai_family假设为AF_UNSPEC。(glibc实现有意与SUSv3背道而驰,他声称如果hints为NULL,那么会将ai_flags假设为0.)

59.10.2 释放addrinfo列表:freeaddrinfo()

        geaddrinfo() 函数会动态的为result引用的所有结构分配内存(图59-3),其结果时调用者必须要在不需要浙西结构时释放他们。使用freeaddrinfo()函数可以方便地在一个步骤中执行这个释放任务。

#include <sys/socket.h>
#include <netdb.h>

void freeaddrinfo(struct addrinfo *result);

        如果需要保留addrinfo结果或者其关联的socket地址结构的一个副本,那么必须要在调用freeaddrinfo()之前复制这些结构。

59.10.3 错误诊断:gai_strerror()

        getaddrinfo()在发生错误时会返回表59-1给出的一个非零错误码。

                       表59-1    getaddrinfo()和getnameinfo()返回的错误码 

   给定比偶59-1中列出的一个错误码,gai_strerror函数会返回一个描述该错误的字符串。通常比上表中描述更加简洁。

        

#include <netdb.h>
const char *gai_strerror(int errcode);
            Returns pointer to string containing error message

        gai_strerror()返回的字符串可以作为应用程序显示错误消息的一部分。

59.10.4 getnameinfo() 函数

        gatnameinfo()函数是getaddrinfo()的逆函数,给定一个socket地址结构(IPv4或IPv6),他会范湖一个包含对应的主机和服务名的字符串或者在无法解析名字时返回一个等价的数值。

        

#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr *addr,socklen_t addrlen,char *host,
                size_t hostlen,char *service,size_t servlen,int flags);
            Returns 0 on success,or nonzero on error

 addr 参数时一个指向待转换的socket地址结构的指针,该结构的长度是由addrlen指定的。通常,addr和addrlen的值是从 accept() 、recvfrom()、getsockname()或者getpeername()调用中获得的。

        得到的主机和服务名是以null为结尾的字符串,他们会被封存在host和service指向的缓冲器中。调用者必须要为这些缓冲器分配空间并将他们的大小传入hostlen和servlen。<metdb.h>头文件定义了两个常量来辅助计算这些缓冲器的大小。NI_MAXHOST指出了返回的主机名字符串的最大字节数,其取值为32。这两个常量没有在SUSv3中得到规定,但所有提供getnameinfo()的UNIX实现都对他们尽心了定义。(从glibc2.8起,必须要定义_BSD_SOURCE、_SVID_SOUCE或GNU_SOURCE中的其中一个特性文本红才能获取NI_MAXHOST和NI_MAXSERV的定义。)

        如果不想获取主机名,那么将host指定为NULL并且将hostlen指定为0,。同样地,如果不需要服务名,那么可以将service指定为NULL并且将servlen指定为0。但是host和service中至少有一个为非NULL值(并且对应的长度参数必须为0)

  最后一个参数flags是一个位掩码,他控制这getnameinfoinfo()的行为,其取值为下面这些常量取OR。

 NI_DGRAM

        在默认情况下,getnameinfo()返回与流socket(即TCP)服务对应的名字。通常,这是无关紧要的,因为正如59.9节中指出的那样,与TCP和UDP端口对应的服务名通常时相同的,但在一些名字不同的场景中,NI_DGRAM标记会强制返回数据报socket服务的名字 

NI_NAMEREQD

        在默认情况下,如果无法解析主机名,那么在host中会返回一个数值地址字符串,如果指定了指定了NI_NAMEREQD,那么就会返回一个错误(EAI_NONAME)。 

NI_NOFQDN

        在默认情况下会返回主机的完全限定名。指定NI_NOFQDN标记会导致主机位于局域网中时只返回名字的第一部分(即主机名)。

NI_NUMERICHOST

        强制在host中返回一个数值地址字符串,这个标记需要便可能耗时较长的DNS服务调用时是比较有用的。 

NI_NUMERICSERV

         强制在service中返回一个十进制端口号字符串。这个标记在知道端口号不对应于服务名时---如他是一个由内核分配给socket的临时端口号 ---以及需要避免不必要的搜索/etc/services的第十小行时比较有用的。

        getnameinfo()在成功时会返回0,失败是会返回表59-1中给出的其中一个非零错误码。

59.11 客户端、服务器示例(流式socket)

           为处理服务器和客户端主机以不同的形式来表示整数的情况,需要减所有传输的整数编码成以换行符结尾的字符串并使用readLine()函数(清单59-1)来读取这些字符串

        服务器程序 

        程序清单59-6给出的服务器程序执行了下列任务。

  •  将服务器的序号初始化为1或通过可选的命令行参数提供的值①
  • 忽略SIGPIPE信号②这样能够防止服务器在尝试向一个对端已经被关闭的socket写入数据时收到SIGPIPE信号;反之,write()会失败并返回EPIPE错误
  • 调用getaddrinfo()④获取端口号PORT_NUM的TCP socket的socket地址结构组。(通常会使用一个服务名,而不会使用一个硬编码的端口号。)这里指定了AI_PASSIVE标记③,这样得到的socket会本绑定到通配地址上(58.5节),其结果时当服务器运行在一个多宿主机上时可以接受发到主机的任意一个昂罗地址上的连接请求。
  • 进入一个循环迭代上一步中返回的socket地址结构⑤.这个循环在程序找到一个能成功用来创建和绑定到一个socket上的地址结构时结束。
  • 在上一步创建的socket设置SO_REUSEADDR选项⑥,有关这个选项的讨论将会放在61.10节中进行,在那一节中将会指出一个TCP服务器通常应该在其监听socket上这只这个选项。
  • 将socket设置为一个监听socket⑧
  • 开启一个无线的for循环⑨以迭代服务客户端。每个客户端的请求会在接受下一个客户端的请求之前得到服务。对于每个客户端,服务器将会执行下列任务。

        --接受一个新连接⑩。服务器向accecpt()的第二个和第三个参数传入了一个非NULL指针以便获取客户端的地址。服务器会在标准输出上显示客户端的地址⑪(IP地址加端口号)。

        --读取客户的消息⑫,该消息由一个换行符结尾的指定了接护短请求的序号数量的字符串构成。服务器将这个字符串扎UN哈UN成一个整数并将其存储在变量reqLen中⑬。

        --将序号的当前值(seqNum)发送给客户端并将该值编码成一个以换行符结尾的字符串⑭。

客户端可以假定他已经分配到了范围在seqNum到(seqNum + reqLen -1)之间的序号。

        -- 将reqLen 加到seqNum 上已更新服务器的序号值⑮

                                //--------------------------is_sequm_sv.c
#include <netinet/in.h>
#include <sys/socket.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>

#define PORT_NUM "50000"       //PORT NUM FOR SERVER

#define INT_LEN 30          // size of string able to hold largest integer(including terminating '\n')

#define _BSD_SOURCE  /*To get definitions iof NI_MAXHOST and NI_MAXSERV from <metdb.h>*/
#define BACKLOG 50

extern ssize_t readLine(int fd,void *buffer,size_t n);
int main(int argc,char *argv[])
{
    uint32_t seqNum;
    char reqLenStr[INT_LEN];
    char seqNumStr[INT_LEN];
    struct sockaddr_storage claddr;
    int lfd,cfd,optval,reqLen;
    socklen_t addrlen;
    struct addrinfo hints;
    struct addrinfo *result,*rp;
#define ADDRSTRLEN (NI_MAXHOST + NI_MAXSERV + 10)
    char addrStr[ADDRSTRLEN];
    char host[NI_MAXHOST];
    char service[NI_MAXSERV];

    if(argc >1 && strcmp(argv[1],"--help") ==0)
        printf("%s [init-seq-num]\n",argv[0]);
    
    seqNum = (argc>1)?atoi(argv[1]):0;//(argv[1],0,"init-seq-num"):0;

    if(signal(SIGPIPE,SIG_IGN) == SIG_ERR)
    {
        printf("signal\n");
        return -1;
    }
    //Call getaddrinfo() to obtain a list of address that we can try binding to
    memset(&hints,0,sizeof(struct addrinfo));
    hints.ai_canonname = NULL;
    hints.ai_addr = NULL;
    hints.ai_next = NULL;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_family = AF_UNSPEC;  //ALOW ipV4 or IPv6
    hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV;
                    /*WildCard ip address; service name is numeriic*/
    if(getaddrinfo(NULL,PORT_NUM,&hints,&result) !=0)
    {
        perror("getaddrinfo");
        return -1;
    }

    /*Walk through returned list until we find an address structure 
    that can be used to successfully create and bind a socket*/

    optval = 1;
    for(rp = result;rp !=NULL;rp->ai_next)
    {
        lfd = socket(rp->ai_family,rp->ai_socktype,rp->ai_protocol);
        if(lfd == -1)
            continue;           //on error try nect address
        if(setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1)
        {
            perror("setsockopt");
            return -1;
        }
        if(bind(lfd,rp->ai_addr,rp->ai_addrlen) == 0)
            break;   //on success;
        /*bind faild :close ths socket and try next address*/

        close(lfd);
    }
    if(rp == NULL)
    {
        printf("fatal: could not bind socket to any address");
    }

    if(listen(lfd,BACKLOG) == -1)
    {
        perror("listen");
        return -1;
    }
    freeaddrinfo(result);

    for(;;)  // hamdle clients ieratively
    { 
        //accept a client connection,botaining client's address
        addrlen = sizeof(struct sockaddr_storage);
        cfd = accept(lfd,(struct sockaddr *)&claddr,&addrlen);
        if(cfd == -1)
        {
            perror("accept");
            continue;
        }

        if(getnameinfo((struct sockaddr *)&claddr,addrlen,
                    host,NI_MAXHOST,service,NI_MAXSERV,0) == 0)
            snprintf(addrStr,ADDRSTRLEN,"(%s,%s)",host , service);
        else
            snprintf(addrStr,ADDRSTRLEN,"(?UNKONWN?)");

        /*read client request, send sequence number back*/
        if(readLine(cfd,reqLenStr,INT_LEN) <=0)
        {
            close(cfd);
            continue;   //failed read,skip request
        }

        reqLen = atoi(reqLenStr);
        if(reqLen <= 0)  // Watch for misbehaving clients
        {
            close(cfd);
            continue;   //Bad request; skip it
        }
        snprintf(seqNumStr,INT_LEN,"%d\n",seqNum);
        if(write(cfd,&seqNumStr,strlen(seqNumStr)) != strlen(seqNumStr))
            fprintf(stderr,"Error on write");
        seqNum += reqLen;   //update  sequence number

        if(close(cfd)==-1)     //Close conection
        {
            printf("close\n");
        }
    }

}

ssize_t readLine(int fd,void *buffer,size_t n)
{
    ssize_t numRead;// # of bytes fetched by last read()
    size_t totRead ;     //total bytes read to far
    char * buf;
    char ch; 
    if(n <0 || buffer == NULL)
    {
        errno = EINVAL;
        return -1;
    }

    buf = buffer;  //No pointer arithmetic on "void *"
    totRead = 0;
    for(;;)
    {
        numRead = read(fd,&ch,1);
        if(numRead == -1)
        {
            if(errno == EINTR) //interrupted ->restart read
                continue;
            else
                return -1;
        }else if(numRead == 0)//EOF
        {
            if(totRead == 0)  //No bytes read; return 0
                return 0;
            else
                break;      //some bytes read add '\0'
        }else{
            if(totRead <n-1){ // numRead must be 1 if we get here
                totRead++;    // discard > (n-1) bytes
                *buf ++ = ch;
            }
            if(ch == '\n')
                break;
        }
    }

    *buf = '\0';
    return totRead;
}

客户端程序

        程序清单59-7给出了客户端程序。这个程序接受两个参数。第一个参数时运行服务器的主机名,该参数是必需的。第二个参数是客户端所需的序号长度,默认长度是1.客户端执行了下列任务。

  • 调用getaddrinfo()获取一组适合连接到 绑定在指定主机上的TCP服务其的socket地址结构①,对于端口号,客户端会将其指定为PORT_NUM。
  • 进入一个循环②遍历上一步中返回的socket地址结构直到客户端找到一个能够成功用来创建③并连接④到服务器socket的地址结构位置。由于客户端不会绑定其socket,因此connect()调用会导致内核为该socket分配一个临时端口。
  • 发送一个整数指定客户端所需的序号长度⑤。这个整合素会被编码成以换行符结尾的字符串发送。
  • 该服务器发送回来的序号(同样也是一个以换行符结尾的字符串)⑥并将其打印到标准输出上⑦。

当一台主机和服务器上运行服务器和客户端会同时看到下列输出。

程序清单59-7

                        --------is_seqnum_cl.c
#include <netinet/in.h>
#include <sys/socket.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>

#define PORT_NUM "50000"       //PORT NUM FOR SERVER
#define INT_LEN 30          // size of string able to hold largest  integer(including terminating '\n')
extern ssize_t readLine(int fd,void *buffer,size_t n);
int main(int argc,char* argv[])
{
    char *reqLenStr;         //request length of sequence
    char seqNumStr[INT_LEN];
    int cfd;
    ssize_t numRead;
    struct addrinfo hints;
    struct addrinfo *result, *rp;

    if(argc <2 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s server-host [sequence-len]\n",argv[0]);
    }
    /*call get addrinfo() to obtain a list of addresses that we can try connecting to*/

    memset(&hints,0,sizeof(struct addrinfo));
    hints.ai_canonname = NULL;
    hints.ai_addr = NULL;
    hints.ai_next = NULL;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_NUMERICSERV;

    if(getaddrinfo(argv[1],PORT_NUM,&hints,&result)!=0)
    {
        perror("getaddrinfo");
        return -1;
    }

    /*Walk through returned list untl until we find an address structure
    that can be used to successfully create and bind a socket*/

    for(rp = result;rp!=NULL;rp=rp->ai_next)
    {
        cfd = socket(rp->ai_family,rp->ai_socktype,rp->ai_protocol);
        if(cfd == -1)
            continue;
        
        if(connect(cfd,rp->ai_addr,rp->ai_addrlen)!=-1)
            break;
        /*Connect failed:close this socket and try next address*/

        close(cfd);
    }

    if(rp == NULL)
    {
        printf("Could not connect socket to any address");
    }

    freeaddrinfo(result);

    //Send request sequence length, with terminatng newline
    reqLenStr = (argc > 2)?argv[2]:"1";
    if(write(cfd,reqLenStr,strlen(reqLenStr))!=strlen(reqLenStr))
    {
        printf("Patial/failed write(reqStr)");
    }
    if(write(cfd,"\n",1) !=1)
    {
        printf("Patial/failed write newline");
    }

    /*Read and display sequence number rerurned by server*/
    numRead = readLine(cfd,seqNumStr,INT_LEN);
    if(numRead == -1)
    {
        printf("readLine");
        return -1;
    }
    if(numRead == 0)
    {
        printf("unexpected EOF from server\n");
    }

    printf("Sequence number: %s",seqNumStr);  //include '\n'
    return 0;              //close  'cfd'
    
}

ssize_t readLine(int fd,void *buffer,size_t n)
{
    ssize_t numRead;// # of bytes fetched by last read()
    size_t totRead ;     //total bytes read to far
    char * buf;
    char ch; 
    if(n <0 || buffer == NULL)
    {
        errno = EINVAL;
        return -1;
    }

    buf = buffer;  //No pointer arithmetic on "void *"
    totRead = 0;
    for(;;)
    {
        numRead = read(fd,&ch,1);
        if(numRead == -1)
        {
            if(errno == EINTR) //interrupted ->restart read
                continue;
            else
                return -1;
        }else if(numRead == 0)//EOF
        {
            if(totRead == 0)  //No bytes read; return 0
                return 0;
            else
                break;      //some bytes read add '\0'
        }else{
            if(totRead <n-1){ // numRead must be 1 if we get here
                totRead++;    // discard > (n-1) bytes
                *buf ++ = ch;
            }
            if(ch == '\n')
                break;
        }
    }

    *buf = '\0';
    return totRead;
}

    59.12 Internet domain socket 库

     本节将使用59.10节中介绍的函数来实现一个函数库,他执行了使用Internet domain socket时碰到的常见任务。(这个库对于59.11给出的示例中很多任务进行了抽象。)由于这些函数使用了协议独立的getaddfrinfo()和getnameinfo()函数。因此他们既可以用于IPv4也可用于IPv6,程序清单给出了声明这些函数的头文件。

        这个库中很多函数都接受类似的参数。

  • host 参数是一个字符串,它包含一个主机名或一个数值地址(以IPv4的点分十进制表示或IPv6的十进制字符串表示)。或者也可以将host指定为NULL来表明使用回环IP地址。
  • service 参数是一个服务名或者是一个以十进制字符串小时的端口号
  • type参数时socket的类型,其取值为SOCK_STREAM或SOCK_DGRAM。

程序清单 59-8 inet_sockets.c使用的头文件 


#define INET_SOCKETS_H
#define INET_SOCKETS_H   //Prevent accidental double inclusion

#include <sys/socket.h>
#include <netdb.h>

int inetConnect(const char*host,const char *service,int type);
    /*Returns a file descriptor on success,or -1 on error*/
/*inetConnect() 函数 根据给定的socket type 创建一个socket并将其连接到host和service指定的地址。
这个函数可供需要将自己的socket连接到一个服务器socket的TCP或UDP 客户端使用
新的文件描述符会作为函数结果返回*/

int inetListen(const char * service,int backlog, socklen_t *addrlen);
    /*Returns a file descriptor on success,or -1 on error*/
    /*inetListen()函数创建一个监听流(SOCK_STREAM)socket,该socket会被绑定到由service指定的
    TCP端口的通配IP地址上,这个函数设计供TCP服务器使用。
    1,新socket的文件描述符回座位函数结果返回
    2,backlog参数指定了允许积压的未决连接数量(与listen()一样)
    3,如果将addrlen 指定为一个非NULL指针,那么与返回的文件描述符对应的socket地址结构的大小会返回他所指向的位置中
    。通过这个值可以在需要湖区一个已连接socket的地址时为传入后面的accept()调用的socket地址缓冲器分配一个合适的大小。*/
int inetBind(const char *service,int type,socklen_t *addrlen);
    /*Returns a file descriptor on success,or -1 on error*/
    /*inetBind()函数根据给定的type创建一个socket并将其绑定到由service和type指定的端口的通配IP地址上。
    (socket type指定了该socke是一个TCP服务器还是一个UDP服务器。)这个函数被设计(主要)供UDP服务器
        和创建socket并将其绑定到某个具体地址上的客户端使用。
    1,新socket的文件描述符会作为函数结果返回
    2,与inetListen()一样,inetBind()会将关联socket地址结构的长度返回在addrlen指向的位置中。这对于需要为传递给recvfrom()
    的缓冲器分配空间以及获取发送数据报的socket的地址来讲是比较有用的。(inetListen()和inetBind()所需做的很多工作是相同
    的,这些工作是通过库中的单个函数inetPassiveSocket()来实现的)*/
int inetAddressStr(const struct sockaddr *addr,socklen_t addrlen,
                    char *addrStr,int addrStrLen);
    /*Returns pointer to addrStr,a string containing host and service name*/
    /*返回一个指向addrStr的指针,该字符串包含了主机和服务名。
    假设在addr中给定了socket地址结构,其长度在addrlen中指定,那么inetAddressStr()
    会返回一个以ull结尾的字符串,该字符串包含了对应的主机名和端口号,其形式如下。
    (hostname,port-number)
    返回的字符串是存放在addrStr指向的缓冲器中的。调用者必须要在addrStrLen中指定这个缓冲器的大小。
    如果返回的字符串超过了(addrStrLen-1)字节,那么它就会被截断。常量IS_ADDR_STR_LEN为addrStr缓冲器
    定义了一个建议值,他的取值一概足以存案所有可能的返回字符串了。inetAddressStr()返回addrStr作为其结果*/

程序清单59-9:一个Inernet domai  socket 库

        

#define _DEFAULT_SOURCE    //To get NI_MAXHOST and NI_MAXSERV definitions from <netdb.h> 

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include "inet_sockets.h"


typedef enum{
    TRUE = 0,
    FALSE
}Boolean;

int inetConnect(const char *host,const char * service,int type)
{
    struct addrinfo hints;
    struct addrinfo *result, *rp;
    int sfd,s;

    memset(&hints,0,sizeof(struct addrinfo));
    hints.ai_canonname = NULL;
    hints.ai_addr  = NULL;
    hints.ai_next = NULL;
    hints.ai_family = AF_UNSPEC;      //Allows IPv4 or IPv6
    hints.ai_socktype = type;

    s = getaddrinfo(host,service,&hints,&result);
    if(s !=0)
    {
        errno = ENOSYS;
        return -1;
    }

    //Walk through returned list until we find an address structure that
    //canbe used to successfully connect a socket

    for(rp = result;rp!=NULL;rp = rp->ai_next)
    {
        sfd = socket(rp->ai_family,rp->ai_socktype,rp->ai_protocol);
        if(sfd == -1)
            continue;

        if(connect(sfd,rp->ai_addr,rp->ai_addrlen) != -1)
            break;

        /*Connect failed:close this socket and try next address*/
        close(sfd);
    } 

    freeaddrinfo(result);

    return (rp == NULL)? -1:sfd;;
}

static int inetPassiveSocket(const char *service,int type,socklen_t *addrlen,
                                Boolean doListen, int backlog)
{
    struct addrinfo hints;
    struct addrinfo *result, *rp;
    int sfd,optval,s;

    memset(&hints,0,sizeof(struct addrinfo));
    hints.ai_canonname = NULL;
    hints.ai_addr  = NULL;
    hints.ai_next = NULL;
    hints.ai_family = AF_UNSPEC;      //Allows IPv4 or IPv6
    hints.ai_flags = AI_PASSIVE;  //USE WILDCARD IP address

    s = getaddrinfo(NULL,service,&hints,&result);
    if(s !=0)
        return -1;
    //Walk through returned list until we find an address structure that
    //canbe used to successfully connect a socket

    optval = 1;

    for(rp = result;rp !=NULL;rp = rp->ai_next){
        sfd = socket(rp->ai_family,rp->ai_socktype,rp->ai_protocol);
        if(sfd == -1)
            continue;

        if(doListen){
            if(setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1)
            {
                close(sfd);
                freeaddrinfo(result);
                return -1;
            }
        }

        if(bind(sfd,rp->ai_addr,rp->ai_addrlen) == 0)
            break;    //sucess
        //bind() failed close this socket and try next address

        close(sfd);
    }
    if(rp !=NULL && doListen){
        if(listen(sfd,backlog) == -1){
             freeaddrinfo(result);
            return -1;
        }
    }
    if(rp != NULL && addrlen != NULL)
        *addrlen = rp->ai_addrlen;   //return address structure size
    freeaddrinfo(result);

    return (rp == NULL)?-1:sfd;

}
int initListen(const char *service,int backlog,socklen_t *addrlen)
{
    return inetPassiveSocket(service,SOCK_STREAM,addrlen,TRUE,backlog);
}

int initBind(const char *service,int type,socklen_t *addrlen)
{
    return inetPassiveSocket(service,type,addrlen,FALSE,0);
}

char * inetAddressStr(const struct sockaddr *addr,socklen_t addrlen,
                        char *addrStr,int addrStrLen)
{
    char host[NI_MAXHOST],service[NI_MAXSERV];
    if(getnameinfo(addr,addrlen,host,NI_MAXHOST,service,NI_MAXSERV,NI_NUMERICSERV) == 0)
        snprintf(addrStr,addrStrLen,"%s , %s",host,service);
    else
        snprintf(addrStr,addrStrLen,"(?UNKOOWN?)");

    addrStr[addrStrLen -1] = '\0'; //ensure result is null-terminated
    
    return addrStr;
}

    59.13 过时的主机和服务转换API

     59.13.1 inet_aton()和inet_ntoa函数

         inet_aton()和inet_ntoa() 函数将一个IPv4地址在点分十进制标记法和二进制形式(以网络字节序)之间转换。这些函数现在已经被inet_pton()和inet_ntop()所取代了。

        inet_aton()("ASCII 到网络")函数将str指向的点分十进制字符串转换成一个网络字节序 的IPv4地址,转换得到的地址将会返回在addr指向的in_adr结构体中。

#include<arpa/inet.h>
int inet_aton(const char *str,struc in_addr * addr)
    Returns 1(true) if str is a valid dotted-decimal address,or 0(false) on error

        inet_aton()函数在转换成功时返回1,在str无效时返回0。

        传入inet_aton()的字符串的数值部分无需是十进制的,它可以是八进制的(通过前导0指定),也可以是十六进制的(通过前导0x或0X指定)。此外inet_aton()还支持简写形式,这样就能够使用少于四个的数值部分类指定一个地址了。术语数字和点标记法用于表示此类采用了这些特性的更通用的地址字符串。

        SUSv3并没有规定inet_aton声明,然而在大多数实现上都存在这个函数。在Liunx上要获取<arpa/inet.h>中的inet_aton()声明就必须要定义_BSD_SOURCE、_SVID_SOURCE或_GUN_SOURCE这三个特性测试宏中的一个

#include <arpa/inet.h>

char *inet_ntoa(struct in_addr addr);
        Returns pointer to(statically allocated) dotted-dcimal string version of addr

        给定一个in_addr结构(一个32位的网络字节序IPv4地址),inet_ntoa()返回一个指向(静态分配的)包含点分十进制标记法标记的地址的字符串的指针。

由于inet_aton()返回的字符串时静态分配的,因此他们会被后续的调用所覆盖

59.13.2 gethostbyname()和gethostbyaddr()函数

        gethostbyname()h和gethostbyaddr()函数允许在主机名和IP地址之间进行转换。现在这些函数已经被getaddrinfo()和getnameinfo()所取代了。

        

#include <netdb.h>

extern int h_errno;

struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const char *addr,socklen_t len,int type);
    Both Return pointer to(statically allocated) hostent structure on success,or NULL on error

        gethostbyname()函数解析由name给出的主机名并返回一个指向静态分配的包含了主机名相关信息的hostent结构的指针。该结构的形式如下。

struct hostent{
    char *h_name;   //Official(canonical) name of host
char **h_aliases;    //NULL-terminated array of pointers to alias strings
int    h_addrtype;    //Address type(AF_INET or AF_INET6)
int    h_length;      //Length (in bytes) of addresses pointed to by h_addr_list
                        //(4 bytes for AF_INET, 16 bytes for AF_INET6)
char **h_addr_list;  /*NULL-terminated array of pointers to host IP 
                        (in_addr or in6_addr structures)in network byte order*/
}
#define h_addr h_addr_list[0]

        h_name字段返回主机的官方的名字,他是一个以null字符结尾的字符串。h_aliases字段指向一个指针数组,数组中的指针指向以nunll结尾的包含了该主机名的别名(可选名)的字符串。

        h_addr_list字段是一个指针数组,数组中的指针事项这个主机的IP地址结构。(一个多宿主机拥有的地址数超过一个。)这个列表由in_addr或in6_addr结构构成,通过h_addrtype字段可以确定这些数据结构的类型,其取值为AF_INET或AF_INET6;通过h_length字段可以确定这些结构的长度。提供h_addr定义是为了与在hostent结构中只返回一个地址的早期实现保持向后兼容,一些既有代码依赖于这个名字(因袭无法感知多宿主机)。

·       在现代版本的gethostbyname()中也可以将name指定为一个数值IP地址字符串,即IPv4的数字和点标记法与IPv6的十六进制字符串标记法。在这种情况下不会执行任何的查询工作;相反,name会被复制到hostent结构的h_name字段,h_addr_list会被设置成name的二进制表示形式。

        gethostbyaddr()函数执行gethostbyname()的逆操作。给定一个二进制IP地址,他会返回一个包含与配置了改地址的主机相关的信息的hostent结构。

        在发生错误时(如无法解析一个名字),gethostbyname()和gethostbyaddr()都会返回一个NULL指针并设置全局变量h_errno.正如其名字所表达的那样,这个变量与errno类似(getbostbyname(3)手册描述了这个变量的可取值),herror()和hstrerror()函数雷速与perror和strerror()。

        herror函数(在标注错误上)显示了在str中给出的字符串,后面跟一个冒号(:),然后在显示一条与当前位于h_errno中的错误对应的信息。或者可以用hstrerrno获取一个指向与在err中指定的错误值对应的字符串的指针。

#define _BSD_SOURCE     /*OR _SVID_SOURCE Or _GNU_SOURCE*/

#include <netdb.h>
void herror(const char *str);
const char *hstrerror(int err);
            Returns pointer to h_errno error string corresponding to err

        程序清单59-10演示了如何使用gethostbyname()。这个程序显示了名字通过命令行指定的各个主机的hotent信息。下面的shell会话演示了这个程序的用法。

$./t_gethostbyname www.baidu.com
Canonical name:www.a.shifen.com
    alias(es):    www.baidu.com
    address type:    AF_INET
address(es):    39.156.66.18 39.156.66.14

程序清单59-10:使用gethostbyname()获取主机信息

#define _DEFAULT_SOURCE
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
    struct hostent *h;
    char **pp;
    char str[INET6_ADDRSTRLEN];

    for(argv++; *argv !=NULL;argv++)
    {
        h = gethostbyname(*argv);
        if(h==NULL){
            fprintf(stderr,"gethostbyname() failed for '%s': %s\n",
                    *argv,hstrerror(h_errno));
            continue;
        }
        printf("Canonical name:%s\n",h->h_name);
        
        printf("    alias(es):    ");
        for(pp = h->h_aliases; *pp !=NULL;pp++)
 		printf("%s",*pp);
           
        printf("\n");
        printf("    address type:    %s\n",
                (h->h_addrtype == AF_INET)?"AF_INET":
                (h->h_addrtype == AF_INET6)?"AF_INET6":"???");
        if(h->h_addrtype == AF_INET || h->h_addrtype == AF_INET6){
            printf("	address(es):    ");
            for(pp = h->h_addr_list;*pp != NULL;pp++)
                printf("%s",inet_ntop(h->h_addrtype,*pp,str,INET6_ADDRSTRLEN));
            printf("\n");
        }
        
    }
    return 0;
}

59.13.3 getserverbyname()和getserverbyport()函数

        getserverbyname()和getserverbyport()函数从/etc/services文件中获取记录。这些函数已经被getaddrinfo()和getnameinfo()所取代了

#include <netdb.h>
struct servent *getservbyname(const char *name,const char *proto);
struct servent *getservbyport(int port,const char *proto);
        Both Return pointer to a(sttaically allocated) servent structure
        on success,or NULL on not found or error

        getservbyname()函数查询服务名(或者其中一个别名)与那么匹配以及协议与protopipei的记录。proto参数是一个诸如tcp或udp之类的字符串,或者也可以将它设置为NULL。如果将proto参数指定为NULL,那么就会返回任意一个服务名与那么匹配的记录。(这种做法通常使用同样的端口号。)如果找到了一个匹配的记录,那么getservbyname会范湖一个指向静态分配的如下礼物几星的结构的指针。

struct servent{
    char *s_name;   //Officail service name
    char **s_aliases;  //pointer to aliases (NULL- terminated)
    int s_port;        //port number (in net work byter order)
    char *s_proto;     //Protocol
};

        一般来讲,调用getserverbyname()只是为了获得端口号,改值会通过a_port字段返回。

getserverbyport()函数执行getservbyname()的逆操作,它返回一个servent记录,该记录包含了/etc/serices文件端口号与port匹配、协议与proto匹配的记录相关的记录。

59.14 UNIX与Internet domain socket比较

        当编写通过网络进行通信的程序必须要使用Internet domain socket,但党委与同一系统上的应用程序使用socket进行通信时则可以选择Internet或UNIX domain socket。在这种情况下应该使用哪个oxket?为何使用这个domain呢?

        编写只是用Internet domain socket 的应用程序通常是最简单的做法,因为这种应用程序既能运行于同一个主机上,也能运行于网络中不同的主机上。但之所以要使用UNIX domain socket是存在几个原因的

  • 在一些实现上,UNIX domain socket的速度比Internet doamin socket的速度快。
  • 可以使用目录(在linux 上是文件),权限来控制岁UNIX domain socket的访问,这样只有运行于指定的用户或组ID下的应用程序才能欧链接到一个监听流socket或想一个数据报socket发送一个数据报,同时为如何验证客户端提供了一个更简单的方法。使用Internet domain socket 时如果需要验证客户端就需要做更多的工作了。
  • 使用UNIX domain socket 可以像61.13.3节中总结的那样传递给打开的文件描述符和发送者的验证消息

59.16 总结

         Internet domain socket 允许位于不同主机上的应用程序通过一个TCP/IP网络进行通信。一个Internet domain socket 地址由一个IP地址和一个端口号构成。在IPv4中,一个IP地址时一个32位的数字,在IPv6中则是一个128位的数字。Internet domain 数据报 socket 运行于UDP上,它提供了无连接的、不可靠的、面向消息的通信。Internet domain 流socket运行于TCP上,他为相互连接的应用程序提供了可靠的、双向字节流通信信道

        不同的计算机架构使用不同的方式来表示数据类型。如整数可以以小端形式存储也可以以大端形式存储,并且不同的计算机可能使用不同的字节数来标书诸如int和long之类的数值类型。这些差别意味着挡在通过网络链接的易购及其之间传输数据时需要采用某种独立于架构的表示。本章指出了存在所中心号编集标准来解决这个问题,同时还描述了很多应用程序所采用的一个简单的解决方案:将所有传输的数据编码成文本形式,字段之间使用预先指定的字符(通常是换行符)分隔。

        本章介绍了一组用于在IP地址的(数值)字符串表示(IPv4是点分十进制,IPv6时十六进制字符串)和其二进制之间的转换函数,然而一般来讲最好使用主机和服务名而不是数字,因为名字更容易记忆并且即使在对应的数字变化时也能继续使用。此外,还介绍了用于将主机和服务名转换成数值表示及其逆过程的各种函数。将主机和服务名转换成socket地址的现代函数是getadrinfo(),但读者在既有代码中会经常看到早期的gethostbyname()和getservbyname()函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值