如何在 IPv4 (AF_INET)和 IPv6 (AF_INET6) domain 中使用 socket 编程? Internet domain socket 地址由一个 IP 地址和一个端口号组成。虽然计算机使用了 IP 地址和端口号的二进制表示形式,但人们对名称的处理能力要比对数字的处理能力强得多。因此,本文将介绍使用名称标识主机计算机和端口的技术。此外,还将介绍如何使用库函数来获取特定主机名的 IP 地址和与特定服务名对应的端口号,其中对主机名的讨论还包括了对域名系统(DNS)的描述,域名系统是一个分布式数据库,它将主机名映射到IP 地址以及将 IP 地址映射到主机名。
1 Internet domain socket
Internet domain 流 socket 是基于 TCP 之上的,它们提供了可靠的双向字节流通信信道。Internet domain 数据报 socket 是基于 UDP 之上的。UDP socket 与之在 UNIX domain 中的对应实体类似,但需要注意下列差别:
- UNIX domain 数据报 socket 是可靠的,但 UDP socket 则是不可靠的—数据报可能会丢失、重复或到达的顺序与它们被发送的顺序不同。
- 在一个 UNIX domain 数据报 socket 上发送数据会在接收 socket 的数据队列为满时阻塞。与之不同的是,使用 UDP 时如果进入的数据报会使接收者的队列溢出,那么数据报就会静默地被丢弃。
2 网络字节序
参考:Linux操作系统中的SOCKET相关 - Socket字节序调整与网络传输
IP 地址和端口号是整数值。在将这些值在网络中传递时碰到的一个问题是不同的硬件结构会以不同的顺序来存储一个多字节整数的字节。从下图可知,存储整数时先存储(即在最小内存地址处)最高有效位的被称为大端,那些先存储最低有效位的被称为小端。小端架构中最值得关注的是 x86。一些硬件结构可以在这两种格式之间切换(ARM 架构既支持大端也支持小端,可通过ARM芯片启动时设置一些系统寄存器来选择所使用的字节序)。在特定主机上使用的字节序被称为主机字节序。
由于端口号和 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);
/* Returns host uint16 converted to network byte order */
uint32_t htonl(uint32_t host_uint32);
/* Returns host _uint32 converted to network byte order */
uint16_t ntohs(uint16_t net_uint16);
/* Returns net uint16 converted to host byte order */
uint32_t ntohl(uint32_t net_uint32);
/* Returns net_uint32 converted to host byte order */
严格地讲,只需要在主机字节序与网络字节序不同的系统上使用这四个函数,但开发人员应该总是使用这些函数,这样程序就能够在不同的硬件结构之间移植了。在主机字节序与网络字节序一样的系统上,这些函数只是简单地原样返回传递给它们的参数。
3 数据表示
在编写网络程序时需要清楚不同的计算机架构使用不同的规则来表示各种数据类型。本章之前已经指出过整数类型可以以大端或小端的形式存储。此外,还存在其他的差别,如 C long数据类型在一些系统中可能是 32 位的,但在其他系统上可能是 64 位的。当考虑结构时,问题就更加复杂了,因为不同的实现采用了不同的规则来将一个结构中的字段对齐到主机系统的地址边界,从而使得字段之间的填充字节数量是不同的。
由于在数据表现上存在这些差异,因此在网络中的异构系统之间交换数据的应用程序必须要采用一些公共规则来编码数据。发送者必须要根据这些规则来对数据进行编码,而接收者则必须要遵循同样的规则对数据进行解码。将数据变成一个标准格式以便在网络上传输的过程被称为信号编集(marshalling)。目前,存在多种信号编集标准,如 XDR(ExterExternalData Representation,在 RFC 1014 中描述)、ASN.1-BER(Abstract SyntaxNotation 1, http://www.asn1.org/)、CORBA以及 XML。一般来讲,这些标准会为每一种数据类型都定义一个固定的格式(如定义了字节序和使用的位数)。除了按照所需的格式进行编码之外,每一个数据项都需要使用额外的字段来标识其类型(以及可能的话还会加上长度)。
然而,一种比信号编集更简单的方法通常会被采用:将所有传输的数据编码成文本形式,其中数据项之间使用特定的字符来分隔开,这个特定的字符通常是换行符。这种方法的一个优点是可以使用 telnet 来调试一个应用程序。要完成这项任务需要使用下面的命令。
telnet host port
接着可以输入一行传给应用程序的文本并查看应用程序发来的响应,在 11 节中将会演示这项技术。
与异构系统在数据表示上的差异相关的问题不仅仅存在于网络间的数据传输中,还存在于此类系统之间的任何数据交换机制中,如在传输异构系统间磁盘或磁带上的文件时会碰到同样的问题。现在,网络编程只不过是可能会碰到这类问题的最常见的编程场景。
如果将在一个流 socket 上传输的数据编码成使用换行符分隔的文本,那么定义一个诸如readLine()之类的函数将是比较便捷的,如下面示例程序所示。
#include "read_line.h"
ssize_t readLine(int fd, void *bufer, size_t n);
/* Returns number of bytes copied into bufer(excluding
terminating null byte), or 0 on end-offile, or -l on error */
readLine()函数从文件描述符参数 fd 引用的文件中读取字节直到碰到换行符为止。输入字节序列将会返回在 buffer 指向的位置处,其中 buffer 指向的内存区域至少为 n 字节。返回的字符串总是以 null 结尾,因此实际上至多有(n–1)个字节会返回。在成功时,readLine()会返回放入 buffer 的数据的字节数,结尾的 null 字节不会计算在内。
/* read_line.c
一次读取一行数据
Implementation of readLine().
*/
#include <unistd.h>
#include <errno.h>
#include "read_line.h" /* Declaration of readLine() */
/* Read characters from 'fd' until a newline is encountered. If a newline
character is not encountered in the first (n - 1) bytes, then the excess
characters are discarded. The returned string placed in 'buf' is
null-terminated and includes the newline character if it was read in the
first (n - 1) bytes. The function return value is the number of bytes
placed in buffer (which includes the newline character if encountered,
but excludes the terminating null byte). */
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 so 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; /* Some other error */
} else if (numRead == 0) { /* EOF */
if (totRead == 0) /* No bytes read; return 0 */
return 0;
else /* Some bytes read; add '\0' */
break;
} else { /* 'numRead' must be 1 if we get here */
if (totRead < n - 1) { /* Discard > (n - 1) bytes */
totRead++;
*buf++ = ch;
}
if (ch == '\n')
break;
}
}
*buf = '\0';
return totRead;
}
如果在遇到换行符之前读取的字节数大于或等于(n–1),那么 readLine()函数会丢弃多余的字节(包括换行符)。如果在前面的(n–1)字节中读取了换行符,那么在返回的字符串中就会包含这个换行符。(因此可以通过检查在返回的 buffer 中结尾 null 字节前是否是一个换行符来确定是否有字节被丢弃了。)采用这种方法之后,将输入以行为单位进行处理的应用程序协议就不会将一个很长的行处理成多行了。当然,这可能会破坏协议,因为两端的应用程序不再同步了。另一种做法是让 readLine()只读取足够的字节数来填充提供的缓冲器,而将到下一行新行为止的剩余字节留给下一个 readLine()调用。在这种情况下,readLine()的调用者需要处理读取部分行的情况。
在 11 节中给出的示例程序中将会使用 readLine()函数。
4 Internet socket 地址
Internet domain socket 地址有两种:IPv4 和 IPv6。
4.1 IPv4 socket 地址:struct sockaddr_in
一个 IPv4 socket 地址会被存储在一个 sockaddr_in 结构中,该结构在<netinet/in.h>中进行定义,具体如下。
struct in_addr { /*IPv4 4-byte address */
in_addr_t s_addr; /* Unsigned 32-bit integer */
};
struct sockaddr_in { /* IPv4 socket address */
sa_family_t sin_family; /* Address family(AF INET)*/
in_port_t sin_port; /* Port number */
struct in_addr sin addr; /* IPv4 address */
unsigned char __pad[X]; /* Pad to size of 'sockaddr' structure(16 bytes)*/
};
普通的 sockaddr 结构中有一个字段来标识 socket domain,该字段对应于 sockaddr_in 结构中的 sin_family 字段,其值总为 AF_INET。sin_port 和 sin_addr 字段是端口号和 IP 地址,它们都是网络字节序的。in_port_t 和 in_addr_t 数据类型是无符号整型,其长度分别为 16 位和 32 位。
4.2 IPv6 socket 地址:struct sockaddr_in6
与 IPv4 地址一样,一个 IPv6 socket 地址包含一个 IP 地址和一个端口号,它们之间的差别在于 IPv6 地址是 128 位而不是 32 位的。一个 IPv6 socket 地址会被存储在一个 sockaddr_in6结构中,该结构在<netinet/in.h>中进行定义,具体如下。
struct in6_addr { /* IPv6 address structure */
uint8_t s6_addr[16]; /* 16 bytes == 128 bits */
};
struct sockaddr_in6 { /* IPv6 socket address */
sa_family_t sin6_family; /* Address family (AF INET6)*/
in_port_t sin6_port; /* Port number */
uint32_t sin6_flowinfo; /* IPv6 flow 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 和 sin6_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}}
在变量声明的初始化器中可以使用 IN6ADDR_ANY_INIT 常量,但无法在一个赋值语句的右边使用这个常量,因为 C 语法并不允许在赋值语句中使用一个结构化的常量。取而代之的做法是必须要使用一个预先定义的变量 in6addr_any,C 库会按照下面的方式对该变量进行初始化。
const struct in6_addr in6addr_any = IN6ADDR_ANY_INIT;
因此可以像下面这样使用通配地址来初始化一个 IPv6 socket 地址。
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(SOME_PORT_NUM);
IPv6 环回地址(::1)的对应常量和变量是 IN6ADDR_LOOPBACK_INIT 和 in6addr _loopback。
与 IPv4 中相应字段不同的是 IPv6 的常量和变量初始化器是网络字节序的,但就像上面给出的代码那样,开发人员仍然必须要确保端口号是网络字节序的。
如果 IPv4 和 IPv6 共存于一台主机上,那么它们将共享同一个端口号空间。这意味着如果一个应用程序将一个 IPv6 socket 绑定到了 TCP 端口 2000 上(使用 IPv6 通配地址),那么 IPv4 TCP socket 将无法绑定到同一个端口上。
4.3 sockaddr_storage 结构(暂时用不到)
在 IPv6 socket API 中新引入了一个通用的 sockaddr_storage 结构,这个结构的空间足以存储任意类型的 socket 地址(即可以将任意类型的 socket 地址结构强制转换并存储在这个结构中)。特别地,这个结构允许透明地存储 IPv4 或 IPv6 socket 地址,从而删除了代码中的 IP 版本依赖性。sockaddr_storage 结构在 Linux 上的定义如下所示。
#define __ss_aligntype uint32_t /* On 32-bit architectures */
struct sockaddr_storage {
sa_family_t ss_family;
__ss_aligntype __ss_align; /* Force alignment */
char __ss_padding[SS_PADSIZE]; /* Pad to 128 bytes */
};
5 主机和服务转换函数概述
计算机以二进制形式来表示 IP 地址和端口号,但人们发现名字比数字更容易记忆。使用符号名还能有效地利用间接关系,用户和程序可以继续使用同一个名字,即使底层的数字值发生了变化也不会受到影响。
主机名和连接在网络上的一个系统(可能拥有多个 IP 地址)的符号标识符。服务名是端口号的符号表示。
主机地址和端口的表示有下列两种方法。
- 主机地址可以表示为一个二进制值或一个符号主机名或展现格式(IPv4 是点分十进制,IPv6 是十六进制字符串)。
- 端口号可以表示为一个二进制值或一个符号服务名。
格式之间的转换工作可以通过各种库函数来完成。本节将对这些函数进行简要的小结。下面几个小节将会详细描述现代 API(inet_ntop()、inet_pton()、getaddrinfo()、getnameinfo()等)。被废弃的 API(inet_aton()、inet_ntoa()、gethostbyname()、getservbyname()等)。
5.1 在二进制和人类可读的形式之间转换 IPv4 地址(已过时)
inet_aton()和 inet_ntoa()函数将一个 IPv4 地址在点分十进制表示形式和二进制表示形式之间进行转换。这里介绍这些函数的主要原因是读者在遗留代码中可能会看到这些函数。现在它们已经被废弃了。需要完成此类转换工作的现代程序应该使用接下来描述的函数。
5.2 在二进制和人类可读的形式之间转换 IPv4 和 IPv6 地址(现代的)
inet_pton()和 inet_ntop()与 inet_aton()和 inet_ntoa()类似,但它们还能处理 IPv6 地址。它们将二进制 IPv4 和 IPv6 地址转换成展现格式—即以点分十进制表示或十六进制字符串表示,或将展现格式转换成二进制 IPv4 和 IPv6 地址。
由于人类对名字的处理能力要比对数字的处理能力强,因此通常偶尔才会在程序中使用这些函数。inet_ntop()的一个用途是产生 IP 地址的一个可打印的表示形式以便记录日志。在有些情况下,最好使用这个函数而不是将一个 IP 地址转换(“解析”)成主机名,其原因如下。
- 将一个 IP 地址解析成主机名可能需要向一台 DNS 服务器发送一个耗时较长的请求。
- 在一些场景中,可能并不存在一个 DNS(PTR)记录将 IP 地址映射到对应的主机名上。
本节在介绍执行二进制表示与对应的符号名之间的转换工作的 getaddrinfo()和getnameinfo()之前先介绍这些函数主要是因为它们提供的更加简单的 API,这样就能快速给出一些正常工作的使用 Internet domain socket 的例子。
5.3 主机和服务名与二进制形式之间的转换(已过时)
gethostbyname()函数返回与主机名对应的二进制 IP 地址,getservbyname()函数返回与服务名对应的端口号。对应的逆向转换是由 gethostbyaddr()和 getservbyport()来完成的。这里之所以要介绍这些函数是因为它们在既有代码中被广泛使用,但现在它们已经过时了。(SUSv3将这些函数标记为过时的,SUSv4 删除了它们的规范。)新代码应该使用 getaddrinfo()和getnameinfo()函数(稍后介绍)来完成此类转换。
5.4 主机和服务名与二进制形式之间的转换(现代的)
getaddrinfo()函数是 gethostbyname()和 getservbyname()两个函数的现代继任者。给定一个主机名和一个服务名,getaddrinfo()会返回一组包含对应的二进制IP地址和端口号的结构。与gethostbyname()不同,getaddrinfo()会透明地处理 IPv4 和 IPv6 地址。因此使用这个函数可以编写不依赖于 IP 版本的程序。所有新代码都应该使用 getaddrinfo()来将主机名和服务名转换成二进制表示。
getnameinfo()函数执行逆向转换,即将一个 IP 地址和端口号转换成对应的主机名和服务名。使用 getaddrinfo()和 getnameinfo()还可以在二进制 IP 地址与其展现格式之间进行转换。
在 10 节中讨论 getaddrinfo()和 getnameinfo()之前需要对 DNS(8 节)和/etc/services文件(9 节)进行描述。DNS 允许协作服务器维护一个将二进制 IP 地址映射到主机名和将主机名映射到二进制 IP 地址的分布式数据库。诸如 DNS 之类的系统的存在对于因特网的运转是非常关键的,因为对浩瀚的因特网主机名进行集中管理是不可能的。/etc/services 文件将端口号映射到符号服务名。
6 inet_pton()和 inet_ntop()函数
inet_pton()和 inet_ntop()函数允许在 IPv4 和 IPv6 地址的二进制形式和点分十进制表示法或十六进制字符串表示法之间进行转换。
#include <arpa/inet.h>
int inet_pton(int domain, const char *src_str, void *addrptr);
/* Returns l on successful conversion, 0 if src_str is not inpresentation format, or -l on error */
const char *inet_ntop(int domain, const void *addrptr, char *dst_str, size_t len);
/* Returns pointer to dst str on success, or NULL on error *、
这些函数名中的 p 表示“展现(presentation)”,n 表示“网络(network)”。展现形式是人类可读的字符串,如:
-
204.152.189.116(IPv4 点分十进制地址);
-
::1(IPv6 冒号分隔的十六进制地址);
-
::FFFF:204.152.189.116(IPv4 映射的 IPv6 地址)。
-
inet_pton()函数将 src_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()在成功时会返回 dst_str。如果 len 的值太小了,那么 inet_ntop()会返回 NULL 并将 errno设置成 ENOSPC。
要正确计算 dst_str 指向的缓冲器的大小可以使用在<netinet/in.h>中定义的两个常量。这些常量标识出了 IPv4 和 IPv6 地址的展现字符串的最大长度(包括结尾的 null 字节)。
#define INET_ADDRSTRLEN 16 /* Maximum IPv4 dotted-decimal string */
#define INET6_ADDRSTRLEN 46 /* Maximum IPv6 hexadecimal string */
7 客户端/服务器示例(数据报 socket)
示例为大小写转换服务器和客户端程序,使用 AF_INET6 domain 中的数据报 socket。头文件定义了服务器的端口号和客户端与服务器可交换的最大消息数量。
/* i6d_ucase.h
Header file for i6d_ucase_sv.c and i6d_ucase_cl.c.
*/
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <ctype.h>
#include "tlpi_hdr.h"
#define BUF_SIZE 10 /* Maximum size of messages exchanged
between client and server */
#define PORT_NUM 50002 /* Server port number */
服务器使用 inet_ntop()函数将客户端的主机地址(通过 recvfrom()调用获得)转换成可打印的形式。
$ ./i6d_ucase_sv &
[1] 1562703
$ ./i6d_ucase_cl ::1 pedro suxi
Server received 5 bytes from (::1, 50234)
Response 1: PEDRO
Server received 4 bytes from (::1, 50234)
Response 2: SUXI
客户端程序与之前的 UNIX domain 中的版本相比存在两个显著的改动。第一个差别在于客户端会将其第一个命令行参数解释成服务器的 IPv6地址。(剩余的命令行参数是作为单独的数据报被传递给服务器的。)客户端使用 inet_pton()将服务器地址转换成二进制形式。另一个差别在于客户端并没有将其 socket 绑定到一个地址上。如果一个 Internet domain socket 没有被绑定到一个地址上,那么内核会将该 socket 绑定到主机系统上的一个临时端口上。这一点可以从下面的 shell 会话日志中看出,其中服务器和客户端运行于同一个主机上。
从上面的输出中可以看出服务器的 recvfrom()调用能够获取客户端 socket 的地址,包括临时端口号,不管客户端是否调用了 bind()。
/* i6d_ucase_sv.c
A server that receives datagrams, converts their contents to uppercase, and
then returns them to the senders.
使用数据报 socket 的 IPv6 大小写转换服务器
See also i6d_ucase_cl.c.
*/
#include "i6d_ucase.h"
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];
/* Create a datagram socket bound to an address in the IPv6 domain */
sfd = socket(AF_INET6, SOCK_DGRAM, 0);
if (sfd == -1)
errExit("socket");
memset(&svaddr, 0, sizeof(struct sockaddr_in6));
svaddr.sin6_family = AF_INET6;
svaddr.sin6_addr = in6addr_any; /* Wildcard address */
svaddr.sin6_port = htons(PORT_NUM);
if (bind(sfd, (struct sockaddr *) &svaddr,
sizeof(struct sockaddr_in6)) == -1)
errExit("bind");
/* Receive messages, convert to uppercase, and return to client */
for (;;) {
len = sizeof(struct sockaddr_in6);
numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr *) &claddr, &len);
if (numBytes == -1)
errExit("recvfrom");
/* Display address of client that sent the message */
if (inet_ntop(AF_INET6, &claddr.sin6_addr, claddrStr,
INET6_ADDRSTRLEN) == NULL)
printf("Couldn't convert client address to string\n");
else
printf("Server received %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)
fatal("sendto");
}
}
/* i6d_ucase_cl.c
使用数据报 socket 的 IPv6 大小写转换客户端
Client for i6d_ucase_sv.c: send each command-line argument as a datagram to
the server, and then display the contents of the server's response datagram.
*/
#include "i6d_ucase.h"
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)
usageErr("%s host-address msg...\n", argv[0]);
/* Create a datagram socket; send to an address in the IPv6 domain */
sfd = socket(AF_INET6, SOCK_DGRAM, 0); /* Create client socket */
if (sfd == -1)
errExit("socket");
memset(&svaddr, 0, sizeof(struct sockaddr_in6));
svaddr.sin6_family = AF_INET6;
svaddr.sin6_port = htons(PORT_NUM); //svaddr.sin6_port 端口-转成网络字节序
if (inet_pton(AF_INET6, argv[1], &svaddr.sin6_addr) <= 0) //svaddr.sin6_addr 地址-转成网络字节序
fatal("inet_pton failed for address '%s'", argv[1]);
/* Send messages 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)
fatal("sendto");
numBytes = recvfrom(sfd, resp, BUF_SIZE, 0, NULL, NULL);
if (numBytes == -1)
errExit("recvfrom");
printf("Response %d: %.*s\n", j - 1, (int) numBytes, resp);
}
exit(EXIT_SUCCESS);
}
8 域名系统(DNS)(了解)
在 10 节中将会介绍获取与一个主机名对应的 IP 地址的 getaddrinfo()函数和执行逆向转换的 getnameinfo()函数,但在介绍这些函数之前需要解释如何使用 DNS 来维护主机名和 IP地址之间的映射关系。
在 DNS 出现以前,主机名和 IP 地址之间的映射关系是在一个手工维护的本地文件/etc/hosts 中进行定义的,该文件包含了形如下面的记录。
$ cat /etc/hosts
127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
gethostbyname()函数(被 getaddrinfo()取代的函数)通过搜索这个文件并找出与规范主机名(即主机的官方或主要名称)或其中一个别名(可选的,以空格分隔)匹配的记录来获取一个IP 地址。
然而,/etc/hosts 模式的扩展性较差,并且随着网络中主机数量的增长,这种方式已经变得不太可行了。
DNS 被设计用来解决这个问题。DNS 的关键想法如下。
- 将主机名组织在一个层级名空间中(下图)。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 服务器自己并没有相关信息来完成解析,那么它就会迭代地解析这个域名。
下面通过一个例子来解释迭代解析。假设本地 DNS 服务器需要解析一个名字www.otago.ac.nz。要完成这个任务,它首先与每个 DNS 服务器都知道的一小组根名字服务器中的一个进行通信。(使用命令 dig . NS 或从网页 http://www.root-servers.org/上可以获取这组服务器列表。)给定名字 www.otago.ac.nz,根名字服务器会告诉本 DNS 服务器到其中一台 nz DNS 服务器上查询。然后本地 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 中定义的。在默认情况下,解析器至少会使用本机的域名来补全。例如,如果登录机器 oghma.otago.ac.nz 并输入了命令 ssh octavo,得到的 DNS 查询将会以 octavo.otago.ac.nz 作为其名字。
9 /etc/services 文件(了解)
众所周知的端口号是由 IANA 集中注册的,其中每个端口都有一个对应的服务名。由于服务号是集中管理并且不会像 IP 地址那样频繁变化,因此没有必要采用DNS 服务器来管理它们。相反,端口号和服务名会记录在文件/etc/services 中。getaddrinfo()和 getnameinfo()函数会使用这个文件中的信息在服务名和端口号之间进行转换。
dockdroid@begoit-916-worker-01:~$ cat /etc/services
# Network services, Internet style
#
# Note that it is presently the policy of IANA to assign a single well-known
# port number for both TCP and UDP; hence, officially ports have two entries
# even if the protocol doesn't support UDP operations.
#
# Updated from https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml .
#
# New ports will be added on request if they have been officially assigned
# by IANA and used in the real-world or are needed by a debian package.
# If you need a huge list of used numbers please install the nmap package.
tcpmux 1/tcp # TCP port service multiplexer
echo 7/tcp
echo 7/udp
discard 9/tcp sink null
discard 9/udp sink null
systat 11/tcp users
daytime 13/tcp
daytime 13/udp
netstat 15/tcp
qotd 17/tcp quote
chargen 19/tcp ttytst source
chargen 19/udp ttytst source
ftp-data 20/tcp
ftp 21/tcp
fsp 21/udp fspd
ssh 22/tcp # SSH Remote Login Protocol
telnet 23/tcp
smtp 25/tcp mail
time 37/tcp timserver
time 37/udp timserver
whois 43/tcp nicname
tacacs 49/tcp # Login Host Protocol (TACACS)
tacacs 49/udp
domain 53/tcp # Domain Name Server
domain 53/udp
bootps 67/udp
bootpc 68/udp
tftp 69/udp
gopher 70/tcp # Internet Gopher
finger 79/tcp
http 80/tcp www # WorldWideWeb HTTP
kerberos 88/tcp kerberos5 krb5 kerberos-sec # Kerberos v5
kerberos 88/udp kerberos5 krb5 kerberos-sec # Kerberos v5
iso-tsap 102/tcp tsap # part of ISODE
acr-nema 104/tcp dicom # Digital Imag. & Comm. 300
pop3 110/tcp pop-3 # POP version 3
sunrpc 111/tcp portmapper # RPC 4.0 portmapper
sunrpc 111/udp portmapper
auth 113/tcp authentication tap ident
nntp 119/tcp readnews untp # USENET News Transfer Protocol
ntp 123/udp # Network Time Protocol
epmap 135/tcp loc-srv # DCE endpoint resolution
netbios-ns 137/tcp # NETBIOS Name Service
netbios-ns 137/udp
netbios-dgm 138/tcp # NETBIOS Datagram Service
netbios-dgm 138/udp
netbios-ssn 139/tcp # NETBIOS session service
netbios-ssn 139/udp
imap2 143/tcp imap # Interim Mail Access P 2 and 4
snmp 161/tcp # Simple Net Mgmt Protocol
snmp 161/udp
snmp-trap 162/tcp snmptrap # Traps for SNMP
snmp-trap 162/udp snmptrap
cmip-man 163/tcp # ISO mgmt over IP (CMOT)
cmip-man 163/udp
cmip-agent 164/tcp
cmip-agent 164/udp
mailq 174/tcp # Mailer transport queue for Zmailer
xdmcp 177/udp # X Display Manager Control Protocol
bgp 179/tcp # Border Gateway Protocol
smux 199/tcp # SNMP Unix Multiplexer
qmtp 209/tcp # Quick Mail Transfer Protocol
z3950 210/tcp wais # NISO Z39.50 database
ipx 213/udp # IPX [RFC1234]
ptp-event 319/udp
ptp-general 320/udp
pawserv 345/tcp # Perf Analysis Workbench
zserv 346/tcp # Zebra server
rpc2portmap 369/tcp
rpc2portmap 369/udp # Coda portmapper
codaauth2 370/tcp
codaauth2 370/udp # Coda authentication server
clearcase 371/udp Clearcase
ldap 389/tcp # Lightweight Directory Access Protocol
ldap 389/udp
svrloc 427/tcp # Server Location
svrloc 427/udp
https 443/tcp # http protocol over TLS/SSL
snpp 444/tcp # Simple Network Paging Protocol
microsoft-ds 445/tcp # Microsoft Naked CIFS
microsoft-ds 445/udp
kpasswd 464/tcp
kpasswd 464/udp
submissions 465/tcp ssmtp smtps urd # Submission over TLS [RFC8314]
saft 487/tcp # Simple Asynchronous File Transfer
isakmp 500/udp # IPSEC key management
rtsp 554/tcp # Real Time Stream Control Protocol
rtsp 554/udp
nqs 607/tcp # Network Queuing system
asf-rmcp 623/udp # ASF Remote Management and Control Protocol
qmqp 628/tcp
ipp 631/tcp # Internet Printing Protocol
#
# UNIX specific services
#
exec 512/tcp
biff 512/udp comsat
login 513/tcp
who 513/udp whod
shell 514/tcp cmd syslog # no passwords used
syslog 514/udp
printer 515/tcp spooler # line printer spooler
talk 517/udp
ntalk 518/udp
route 520/udp router routed # RIP
gdomap 538/tcp # GNUstep distributed objects
gdomap 538/udp
uucp 540/tcp uucpd # uucp daemon
klogin 543/tcp # Kerberized `rlogin' (v5)
kshell 544/tcp krcmd # Kerberized `rsh' (v5)
dhcpv6-client 546/udp
dhcpv6-server 547/udp
afpovertcp 548/tcp # AFP over TCP
nntps 563/tcp snntp # NNTP over SSL
submission 587/tcp # Submission [RFC4409]
ldaps 636/tcp # LDAP over SSL
ldaps 636/udp
tinc 655/tcp # tinc control port
tinc 655/udp
silc 706/tcp
kerberos-adm 749/tcp # Kerberos `kadmin' (v5)
#
domain-s 853/tcp # DNS over TLS [RFC7858]
domain-s 853/udp # DNS over DTLS [RFC8094]
rsync 873/tcp
ftps-data 989/tcp # FTP over SSL (data)
ftps 990/tcp
telnets 992/tcp # Telnet over SSL
imaps 993/tcp # IMAP over SSL
pop3s 995/tcp # POP-3 over SSL
#
# From ``Assigned Numbers'':
#
#> The Registered Ports are not controlled by the IANA and on most systems
#> can be used by ordinary user processes or programs executed by ordinary
#> users.
#
#> Ports are used in the TCP [45,106] to name the ends of logical
#> connections which carry long term conversations. For the purpose of
#> providing services to unknown callers, a service contact port is
#> defined. This list specifies the port used by the server process as its
#> contact port. While the IANA can not control uses of these ports it
#> does register or list uses of these ports as a convienence to the
#> community.
#
socks 1080/tcp # socks proxy server
proofd 1093/tcp
rootd 1094/tcp
openvpn 1194/tcp
openvpn 1194/udp
rmiregistry 1099/tcp # Java RMI Registry
lotusnote 1352/tcp lotusnotes # Lotus Note
ms-sql-s 1433/tcp # Microsoft SQL Server
ms-sql-s 1433/udp
ms-sql-m 1434/tcp # Microsoft SQL Monitor
ms-sql-m 1434/udp
ingreslock 1524/tcp
datametrics 1645/tcp old-radius
datametrics 1645/udp old-radius
sa-msg-port 1646/tcp old-radacct
sa-msg-port 1646/udp old-radacct
kermit 1649/tcp
groupwise 1677/tcp
l2f 1701/udp l2tp
radius 1812/tcp
radius 1812/udp
radius-acct 1813/tcp radacct # Radius Accounting
radius-acct 1813/udp radacct
cisco-sccp 2000/tcp # Cisco SCCP
nfs 2049/tcp # Network File System
nfs 2049/udp # Network File System
gnunet 2086/tcp
gnunet 2086/udp
rtcm-sc104 2101/tcp # RTCM SC-104 IANA 1/29/99
rtcm-sc104 2101/udp
gsigatekeeper 2119/tcp
gris 2135/tcp # Grid Resource Information Server
cvspserver 2401/tcp # CVS client/server operations
venus 2430/tcp # codacon port
venus 2430/udp # Venus callback/wbc interface
venus-se 2431/tcp # tcp side effects
venus-se 2431/udp # udp sftp side effect
codasrv 2432/tcp # not used
codasrv 2432/udp # server port
codasrv-se 2433/tcp # tcp side effects
codasrv-se 2433/udp # udp sftp side effect
mon 2583/tcp # MON traps
mon 2583/udp
dict 2628/tcp # Dictionary server
f5-globalsite 2792/tcp
gsiftp 2811/tcp
gpsd 2947/tcp
gds-db 3050/tcp gds_db # InterBase server
icpv2 3130/tcp icp # Internet Cache Protocol
icpv2 3130/udp icp
isns 3205/tcp # iSNS Server Port
isns 3205/udp # iSNS Server Port
iscsi-target 3260/tcp
mysql 3306/tcp
ms-wbt-server 3389/tcp
nut 3493/tcp # Network UPS Tools
nut 3493/udp
distcc 3632/tcp # distributed compiler
distcc 3632/udp
daap 3689/tcp # Digital Audio Access Protocol
daap 3689/udp
svn 3690/tcp subversion # Subversion protocol
svn 3690/udp subversion
suucp 4031/tcp # UUCP over SSL
suucp 4031/udp
sysrqd 4094/tcp # sysrq daemon
sysrqd 4094/udp
sieve 4190/tcp # ManageSieve Protocol
epmd 4369/tcp # Erlang Port Mapper Daemon
epmd 4369/udp
remctl 4373/tcp # Remote Authenticated Command Service
remctl 4373/udp
f5-iquery 4353/tcp # F5 iQuery
f5-iquery 4353/udp
ipsec-nat-t 4500/udp # IPsec NAT-Traversal [RFC3947]
iax 4569/tcp # Inter-Asterisk eXchange
iax 4569/udp
mtn 4691/tcp # monotone Netsync Protocol
mtn 4691/udp
radmin-port 4899/tcp # RAdmin Port
radmin-port 4899/udp
sip 5060/tcp # Session Initiation Protocol
sip 5060/udp
sip-tls 5061/tcp
sip-tls 5061/udp
xmpp-client 5222/tcp jabber-client # Jabber Client Connection
xmpp-server 5269/tcp jabber-server # Jabber Server Connection
cfengine 5308/tcp
mdns 5353/udp # Multicast DNS
postgresql 5432/tcp postgres # PostgreSQL Database
freeciv 5556/tcp rptp # Freeciv gameplay
amqps 5671/tcp # AMQP protocol over TLS/SSL
amqp 5672/tcp
amqp 5672/udp
amqp 5672/sctp
x11 6000/tcp x11-0 # X Window System
x11-1 6001/tcp
x11-2 6002/tcp
x11-3 6003/tcp
x11-4 6004/tcp
x11-5 6005/tcp
x11-6 6006/tcp
x11-7 6007/tcp
gnutella-svc 6346/tcp # gnutella
gnutella-svc 6346/udp
gnutella-rtr 6347/tcp # gnutella
gnutella-rtr 6347/udp
sge-qmaster 6444/tcp sge_qmaster # Grid Engine Qmaster Service
sge-execd 6445/tcp sge_execd # Grid Engine Execution Service
mysql-proxy 6446/tcp # MySQL Proxy
babel 6696/udp # Babel Routing Protocol
ircs-u 6697/tcp # Internet Relay Chat via TLS/SSL
afs3-fileserver 7000/tcp bbs # file server itself
afs3-fileserver 7000/udp bbs
afs3-callback 7001/tcp # callbacks to cache managers
afs3-callback 7001/udp
afs3-prserver 7002/tcp # users & groups database
afs3-prserver 7002/udp
afs3-vlserver 7003/tcp # volume location database
afs3-vlserver 7003/udp
afs3-kaserver 7004/tcp # AFS/Kerberos authentication
afs3-kaserver 7004/udp
afs3-volser 7005/tcp # volume managment server
afs3-volser 7005/udp
afs3-errors 7006/tcp # error interpretation service
afs3-errors 7006/udp
afs3-bos 7007/tcp # basic overseer process
afs3-bos 7007/udp
afs3-update 7008/tcp # server-to-server updater
afs3-update 7008/udp
afs3-rmtsys 7009/tcp # remote cache manager service
afs3-rmtsys 7009/udp
font-service 7100/tcp xfs # X Font Service
http-alt 8080/tcp webcache # WWW caching service
puppet 8140/tcp # The Puppet master service
bacula-dir 9101/tcp # Bacula Director
bacula-fd 9102/tcp # Bacula File Daemon
bacula-sd 9103/tcp # Bacula Storage Daemon
xmms2 9667/tcp # Cross-platform Music Multiplexing System
nbd 10809/tcp # Linux Network Block Device
zabbix-agent 10050/tcp # Zabbix Agent
zabbix-trapper 10051/tcp # Zabbix Trapper
amanda 10080/tcp # amanda backup services
dicom 11112/tcp
hkp 11371/tcp # OpenPGP HTTP Keyserver
db-lsp 17500/tcp # Dropbox LanSync Protocol
dcap 22125/tcp # dCache Access Protocol
gsidcap 22128/tcp # GSI dCache Access Protocol
wnn6 22273/tcp # wnn6
#
# Datagram Delivery Protocol services
#
rtmp 1/ddp # Routing Table Maintenance Protocol
nbp 2/ddp # Name Binding Protocol
echo 4/ddp # AppleTalk Echo Protocol
zip 6/ddp # Zone Information Protocol
#=========================================================================
# The remaining port numbers are not as allocated by IANA.
#=========================================================================
# Kerberos (Project Athena/MIT) services
kerberos4 750/udp kerberos-iv kdc # Kerberos (server)
kerberos4 750/tcp kerberos-iv kdc
kerberos-master 751/udp kerberos_master # Kerberos authentication
kerberos-master 751/tcp
passwd-server 752/udp passwd_server # Kerberos passwd server
krb-prop 754/tcp krb_prop krb5_prop hprop # Kerberos slave propagation
zephyr-srv 2102/udp # Zephyr server
zephyr-clt 2103/udp # Zephyr serv-hm connection
zephyr-hm 2104/udp # Zephyr hostmanager
iprop 2121/tcp # incremental propagation
supfilesrv 871/tcp # Software Upgrade Protocol server
supfiledbg 1127/tcp # Software Upgrade Protocol debugging
#
# Services added for the Debian GNU/Linux distribution
#
poppassd 106/tcp # Eudora
poppassd 106/udp
moira-db 775/tcp moira_db # Moira database
moira-update 777/tcp moira_update # Moira update protocol
moira-ureg 779/udp moira_ureg # Moira user registration
spamd 783/tcp # spamassassin daemon
skkserv 1178/tcp # skk jisho server port
predict 1210/udp # predict -- satellite tracking
rmtcfg 1236/tcp # Gracilis Packeten remote config server
xtel 1313/tcp # french minitel
xtelw 1314/tcp # french minitel
support 1529/tcp # GNATS
cfinger 2003/tcp # GNU Finger
frox 2121/tcp # frox: caching ftp proxy
zebrasrv 2600/tcp # zebra service
zebra 2601/tcp # zebra vty
ripd 2602/tcp # ripd vty (zebra)
ripngd 2603/tcp # ripngd vty (zebra)
ospfd 2604/tcp # ospfd vty (zebra)
bgpd 2605/tcp # bgpd vty (zebra)
ospf6d 2606/tcp # ospf6d vty (zebra)
ospfapi 2607/tcp # OSPF-API
isisd 2608/tcp # ISISd vty (zebra)
afbackup 2988/tcp # Afbackup system
afbackup 2988/udp
afmbackup 2989/tcp # Afmbackup system
afmbackup 2989/udp
fax 4557/tcp # FAX transmission service (old)
hylafax 4559/tcp # HylaFAX client-server protocol (new)
distmp3 4600/tcp # distmp3host daemon
munin 4949/tcp lrrd # Munin
enbd-cstatd 5051/tcp # ENBD client statd
enbd-sstatd 5052/tcp # ENBD server statd
pcrd 5151/tcp # PCR-1000 Daemon
noclog 5354/tcp # noclogd with TCP (nocol)
noclog 5354/udp # noclogd with UDP (nocol)
hostmon 5355/tcp # hostmon uses TCP (nocol)
hostmon 5355/udp # hostmon uses UDP (nocol)
rplay 5555/udp # RPlay audio service
nrpe 5666/tcp # Nagios Remote Plugin Executor
nsca 5667/tcp # Nagios Agent - NSCA
mrtd 5674/tcp # MRT Routing Daemon
bgpsim 5675/tcp # MRT Routing Simulator
canna 5680/tcp # cannaserver
syslog-tls 6514/tcp # Syslog over TLS [RFC5425]
sane-port 6566/tcp sane saned # SANE network scanner daemon
ircd 6667/tcp # Internet Relay Chat
zope-ftp 8021/tcp # zope management by ftp
tproxy 8081/tcp # Transparent Proxy
omniorb 8088/tcp # OmniORB
omniorb 8088/udp
clc-build-daemon 8990/tcp # Common lisp build daemon
xinetd 9098/tcp
mandelspawn 9359/udp mandelbrot # network mandelbrot
git 9418/tcp # Git Version Control System
zope 9673/tcp # zope server
webmin 10000/tcp
kamanda 10081/tcp # amanda backup services (Kerberos)
amandaidx 10082/tcp # amanda backup services
amidxtape 10083/tcp # amanda backup services
smsqp 11201/tcp # Alamin SMS gateway
smsqp 11201/udp
xpilot 15345/tcp # XPilot Contact Port
xpilot 15345/udp
sgi-cmsd 17001/udp # Cluster membership services daemon
sgi-crsd 17002/udp
sgi-gcd 17003/udp # SGI Group membership daemon
sgi-cad 17004/tcp # Cluster Admin daemon
isdnlog 20011/tcp # isdn logging system
isdnlog 20011/udp
vboxd 20012/tcp # voice box system
vboxd 20012/udp
binkp 24554/tcp # binkp fidonet protocol
asp 27374/tcp # Address Search Protocol
asp 27374/udp
csync2 30865/tcp # cluster synchronization tool
dircproxy 57000/tcp # Detachable IRC Proxy
tfido 60177/tcp # fidonet EMSI over telnet
fido 60179/tcp # fidonet EMSI over TCP
# Local services
协议通常是 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 则是使用了 UDP 端口 514。这是因为这些端口在采用现行的 IANA 策略之前就分配出去了。
/etc/services 文件仅仅记录着名字到数字的映射关系。它不是一种预留机制:在/etc/services中存在一个端口号并不能保证在实际环境中特定的服务就能够绑定到该端口上。
10 独立于协议的主机和服务转换(待续)
getaddrinfo()函数将主机和服务名转换成 IP 地址和端口号,它作为过时的 gethostbyname()和 getservbyname()函数的(可重入的)接替者被定义在了 POSIX.1g 中。(使用 getaddrinfo()替换 gethostbyname()能够从程序中删除 IPv4 与 IPv6 的依赖关系。)
getnameinfo()函数是 getaddrinfo()的逆函数,它将一个 socket 地址结构(IPv4 或 IPv6)转换成包含对应主机和服务名的字符串。这个函数是过时的 gethostbyaddr()和 getservbyport()函数的(可重入的)等价物。
11 客户端/服务器示例(流式 socket)
一个简单的使用 TCP socket 的客户端/服务器应用程序。
11.1 公共头文件
这个文件包含了其他各种头文件并定义了应用程序使用的 TCP 端口号。
/* is_seqnum.h
Header file for is_seqnum_sv.c and is_seqnum_cl.c.
*/
#include <netinet/in.h>
#include <sys/socket.h>
#include <signal.h>
#include "read_line.h" /* Declaration of readLine() */
#include "tlpi_hdr.h"
#define PORT_NUM "50000" /* Port number for server */
#define INT_LEN 30 /* Size of string able to hold largest
integer (including terminating '\n') */
11.2 服务器程序
服务器程序执行了下列任务。
- 将服务器的序号初始化为 1 或通过可选的命令行参数提供的值。
- 忽略 SIGPIPE 信号。这样就能够防止服务器在尝试向一个对端已经被关闭的 socket写入数据时收到 SIGPIPE 信号;反之,write()会失败并返回 EPIPE 错误。
- 调用 getaddrinfo()获取使用端口号 PORT_NUM 的 TCP socket 的 socket 地址结构组。(通常会使用一个服务名,而不会使用一个硬编码的端口号。)这里指定了 AI_PASSIVE标记,这样得到的 socket 会被绑定到通配地址上,其结果是当服务器运行在一个多宿主机上时可以接受发到主机的任意一个网络地址上的连接请求。
- 进入一个循环迭代上一步中返回的 socket 地址结构。这个循环在程序找到一个能成功地用来创建和绑定到一个 socket 上的地址结构时结束。
- 在上一步创建的socket上设置SO_REUSEADDR选项。有关这个选项的讨论将会放在61.10节中进行,在那一节中将会指出一个TCP 服务器通常应该在其监听socket 上设置这个选项。
- 将 socket 标记成一个监听 socket。
- 开启一个无限的 for 循环以迭代服务客户端。每个客户端的请求会在接受下一个客户端的请求之前得到服务。对于每个客户端,服务器将会执行下列任务。
- 接受一个新连接。服务器向 accept()的第二个和第三个参数传入了一个非 NULL 指针以便获取客户端的地址。服务器会在标准输出上显示客户端的地址(IP 地址加上端口号)。
- 读取客户端的消息 ,该消息由一个以换行符结尾的指定了客户端请求的序号数量的字符串构成。服务器将这个字符串转换成一个整数并将其存储在变量 reqLen 中 。
- 将序号的当前值(seqNum)发回给客户端并将该值编码成一个以换行符结尾的字符串 。客户端可以假定它已经分配到了范围在 seqNum 到(seqNum + reqLen–1)之间的序号。
- 将 reqLen 加到 seqNum 上以更新服务器的序号值 。
/* is_seqnum_sv.c
A simple Internet stream socket server. Our service is to provide
unique sequence numbers to clients.
使用流 socket 与客户端进行通信的迭代式服务器
Usage: is_seqnum_sv [init-seq-num]
(default = 0)
See also is_seqnum_cl.c.
*/
#define _BSD_SOURCE /* To get definitions of NI_MAXHOST and
NI_MAXSERV from <netdb.h> */
#include <netdb.h>
#include "is_seqnum.h"
#define BACKLOG 50
int
main(int argc, char *argv[])
{
uint32_t seqNum;
char reqLenStr[INT_LEN]; /* Length of requested sequence */
char seqNumStr[INT_LEN]; /* Start of granted sequence */
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)
usageErr("%s [init-seq-num]\n", argv[0]);
seqNum = (argc > 1) ? getInt(argv[1], 0, "init-seq-num") : 0;
/* Ignore the SIGPIPE signal, so that we find out about broken connection
errors via a failure from write(). */
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) errExit("signal");
/* Call getaddrinfo() to obtain a list of addresses 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; /* Allows IPv4 or IPv6 */
hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV;
/* Wildcard IP address; service name is numeric */
if (getaddrinfo(NULL, PORT_NUM, &hints, &result) != 0)
errExit("getaddrinfo");
/* 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 = rp->ai_next) {
lfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (lfd == -1)
continue; /* On error, try next address */
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))
== -1)
errExit("setsockopt");
if (bind(lfd, rp->ai_addr, rp->ai_addrlen) == 0)
break; /* Success */
/* bind() failed: close this socket and try next address */
close(lfd);
}
if (rp == NULL)
fatal("Could not bind socket to any address");
if (listen(lfd, BACKLOG) == -1)
errExit("listen");
freeaddrinfo(result);
for (;;) { /* Handle clients iteratively */
/* Accept a client connection, obtaining client's address */
addrlen = sizeof(struct sockaddr_storage);
cfd = accept(lfd, (struct sockaddr *) &claddr, &addrlen);
if (cfd == -1) {
errMsg("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, "(?UNKNOWN?)");
printf("Connection from %s\n", addrStr);
/* 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 connection */
errMsg("close");
}
}
11.3 客户端程序
客户端程序接受两个参数。第一个参数是运行服务器的主机名,该参数是必需的。第二个可选的参数是客户端所需的序号长度。默认的长度是 1。客户端执行了下列任务。
- 调用 getaddrinfo()获取一组适合连接到绑定在指定主机上的 TCP 服务器的 socket 地址结构。对于端口号,客户端会将其指定为 PORT_NUM。
- 进入一个循环遍历上一步中返回的socket地址结构直到客户端找到一个能够成功用来创建并连接到服务器 socket 的地址结构为止。由于客户端不会绑定其 socket,因此 connect()调用会导致内核为该 socket 分配一个临时端口。
- 发送一个整数指定客户端所需的序号长度。这个整数将会被编码成以换行符结尾的字符串来发送。
- 读取服务器发送回来的序号(同样也是一个以换行符结尾的字符串)并将其打印到标准输出上。
当在同一台主机上运行服务器和客户端上时会看到下列输出。
$ ./is_seqnum_sv &
[2] 1616625
$ ./is_seqnum_cl localhost
Connection from (localhost, 48194)
Sequence number: 0
$ ./is_seqnum_cl localhost 10
Connection from (localhost, 51390)
Sequence number: 1
$ ./is_seqnum_cl localhost
Connection from (localhost, 59270)
Sequence number: 11
下面演示了如何使用 telnet 来调试这个应用程序。
$ telnet localhost 50000
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Connection from (localhost, 42602)
1
12
Connection closed by foreign host.
/* is_seqnum_cl.c
A simple Internet stream socket client. This client requests a sequence
number from the server.
使用流 socket 的客户端
See also is_seqnum_sv.c.
*/
#include <netdb.h>
#include "is_seqnum.h"
int
main(int argc, char *argv[])
{
char *reqLenStr; /* Requested length of sequence */
char seqNumStr[INT_LEN]; /* Start of granted sequence */
int cfd;
ssize_t numRead;
struct addrinfo hints;
struct addrinfo *result, *rp;
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s server-host [sequence-len]\n", argv[0]);
/* Call getaddrinfo() 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; /* Allows IPv4 or IPv6 */
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_NUMERICSERV;
if (getaddrinfo(argv[1], PORT_NUM, &hints, &result) != 0)
errExit("getaddrinfo");
/* Walk through returned list until we find an address structure
that can be used to successfully connect 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; /* On error, try next address */
if (connect(cfd, rp->ai_addr, rp->ai_addrlen) != -1)
break; /* Success */
/* Connect failed: close this socket and try next address */
close(cfd);
}
if (rp == NULL)
fatal("Could not connect socket to any address");
freeaddrinfo(result);
/* Send requested sequence length, with terminating newline */
reqLenStr = (argc > 2) ? argv[2] : "1";
if (write(cfd, reqLenStr, strlen(reqLenStr)) != strlen(reqLenStr))
fatal("Partial/failed write (reqLenStr)");
if (write(cfd, "\n", 1) != 1)
fatal("Partial/failed write (newline)");
/* Read and display sequence number returned by server */
numRead = readLine(cfd, seqNumStr, INT_LEN);
if (numRead == -1)
errExit("readLine");
if (numRead == 0)
fatal("Unexpected EOF from server");
printf("Sequence number: %s", seqNumStr); /* Includes '\n' */
exit(EXIT_SUCCESS); /* Closes 'cfd' */
}
12 Internet domain socket 库
13 过时的主机和服务转换 API
13.1 inet_aton()和 inet_ntoa()函数
13.2 gethostbyname()和 gethostbyaddr()函数
13.3 getserverbyname()和 getserverbyport()函数
13 UNIX 与 Internet domain socket 比较
当编写通过网络进行通信的应用程序时必须要使用 Internet domain socket,但当位于同一系统上的应用程序使用 socket 进行通信时则可以选择使用 Internet 或 UNIX domain socket。在这种情况下该使用哪个 domain?为何使用这个 domain 呢?
编写只使用 Internet domain socket 的应用程序通常是最简单的做法,因为这种应用程序既能运行于同一个主机上,也能运行在网络中的不同主机上。但之所以要选择使用 UNIX domain socket 是存在几个原因的。
- 在一些实现上,UNIX domain socket 的速度比 Internet domain socket 的速度快。
- 可以使用目录(在 Linux 上是文件)权限来控制对 UNIX domain socket 的访问,这样只有运行于指定的用户或组 ID 下的应用程序才能够连接到一个监听流 socket 或向一个数据报 socket 发送一个数据报,同时为如何验证客户端提供了一个简单的方法。使用Internet domain socket 时如果需要验证客户端的话就需要做更多的工作了。
- 使用 UNIX domain socket 可以传递打开的文件描述符和发送者的验证信息。