网络程序设计教材第五章 名字与地址转换
![网络程序设计教材第五章 名字与地址转换](https://img.360docs.net/imga0/04tjbsus16b5cjyy2lrj-01.webp)
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
第五章名字与地址转换
5.1 域名系统
尽管通过IP地址可以唯一地识别主机上的网络接口,从而访问网络中的每个主机。但是,由于IP地址不便于人们记忆,因此人们还是习惯于使用主机名来访问网络。随着IPv6协议的不断广泛应用,数值地址变得更长,手工键入一个地址更容易出错。
在大多数操作系统中,任何应用程序都可以调用一个标准的库函数来查看给定名字的主机的IP地址(UNIX系统中函数名);同样,系统还提供了一个逆函数,即给定主机的IP地址,查看它所对应的主机名。大多数使用主机名作为参数的应用程序也支持把IP地址作为参数。
域名系统DNS(Domain Name System)主要用于主机名与IP地址间的映射。主机名可以是简单名字,例如Jida,也可以是全限定域名FQDN(Fully Qualified Domain Name),例如。严格地说,FQDN也称为绝对名字(absolute name),因此必须以一个点号结尾,但用户经常省略最后的点号。
在Internet上使用了基于层次型的名字管理机制。在Internet的层次型名字管理中,先由中央管理机构(例如Internet的NIC)将最高一级的名字空间进行划分,并将相应部分的管理权交给相应的机构,各管理机构可对名字空间进行进一步的划分。
一般来说,最高一级的名字空间的划分是基于“网络节点名”(site name)的。网络节点的概念是对Internet整个网络的一部分(通常由若干个网络构成)的一种抽象,这些网络的组织关系或地理位置联系非常紧密,可以将它们看成一个节点。
各个网络节点中又可划分成不同的管理组。组名下面是主机的本地名。典型的Internet 层次型主机名由三部分构成:
本地名称.管理组名.网络节点名
例如,其中cn属于第一级的名字空间,代表国家;第二级域为,代表教育机构,最低一级代表吉林大学。
5.1.1 资源记录
在TCP/IP环境中,域名系统(DNS)是一个分布式数据库系统,通过它来提供IP地址和主机名之间的映射。DNS中的条目称为资源记录RR(resource record),仅有少数几类RR会影响名字与地址转换。
A A记录将主机名映射为32位的IPv4地址。例如,这里有域中关于主机solaris的四个DNS记录,其中第一个就是一个A记录:
Solaris IN A 206.62.226.33
IN AAAA 5f1b:df00:ce3e:e200:0020:0800:2078:e3e3
IN MX 5 .
IN MX 10 .
AAAA AAAA记录将主机名映射为128位的IPv6地址。
PTR PTR记录(称为“指针记录”)将IP地址映射为主机名。对于IPv4地址,32位地址的四个字节顺序反转,每个字节都转换成它的十进制ASCII值(0到255),然后附上in-addr.arpa,结果串用于PTR查询。
对于IPv6地址,128位地址中的32个4位组顺序反转,每组被转换成相应的十六进制ASCII值(0到9,a到f),并附上ip6.int.
例如,主机solaris的两个PTR记录为:33.226.62.206.in-addr.arpa和3.e.3.e.8.7.0.2.0.0.8.0.0.2.0.0.0.0.2.e.e.3.e.c.0.0.f.d.b.1.f.5.ip6.int。
MX MX记录指定一个主机作为某主机的“邮件交换器”。在上面主机的solaris例子中,提供了两个MX记录,第一个记录的优先级是5,第二个记录的优先级是10,当有多个MX记录存在时,需按优先级值的顺序使用,从最小值开始。
CNAME CNAME代表“canonical name(规范名字)”,其常见的用法是为常用服务如WWW 和FTP指派一个CNAME记录。如果用户使用这些服务名而不是实际上的主机名,则它在服务挪到其它主机上时是透明的。例如,主机Jida的CNAME如下:
www IN CNAME
ftp IN CNAME
mailhost IN CNAME
5.1.2 解析器和名字服务器
组织运行一个或多个名字服务器(name server),它们通常就是所说的BIND(Berkeley Internet Name Domain)程序。任何应用程序,通过调用称为解析器(resolver)的库中的函数来与DNS服务器联系。应用程序用来将主机名转换为IP地址或进行相反过程的一组函数称为解析器。最常见的解析器函数是gethostbyname和gethostbyaddr,前者将主机名映射为IP地址,后者执行相反的映射。
图5-1说明了应用进程、解析器和名字服务器的典型关系。解析器代码包含在系统库中,在构造应用程序时被链接到应用程序中。应用程序代码使用正常的函数调用来调用解析器代码,最典型的就是调用函数gethostbyname和gethostbyaddr。
解析器代码读其依赖于系统的配置文件来确定组织的名字服务器们的所在位置。文件/etc/resolv.conf一般包含本地名字服务器的IP地址。
解析器使用UDP给本地名字服务器发查询,如果本地名字服务器不知道答案,它也可
以使用UDP在整个因特网上给其它名字服务器发查询。
5.1.3 DNS替代方法
不使用DNS也可以得到主机的名字和地址信息,最常用的替代方法为静态主机文件(一般为文件/etc/hosts)或网络信息系统NIS(Network Information System)。这里需要注意的是,管理员如何配置一个主机来使用不同的名字服务是依赖于不同的实现的,Solaris2.x和HP-UX10.30使用/etc.nsswitch.conf,Digital Unix使用文件/etc/svc.conf,,IBM AIX使用文件/etc/netsvc.conf。BIND8.1提供了自己的名字为信息检索服务IRS(Information Retrival Service)的版本,它使用文件/etc/irs.conf。如果一个名字服务器将为主机名查找所用,则所有这些系统都使用文件/etc/resolv.conf来指定此名字服务器的IP地址。这些差异一般对应用程序开发人员透明,因此,用户只需调用gethostbyname和gethostbyaddr这样的解析器函数就可以了。
5.2 gethostbyname函数
计算机主机通常以人们可读的名字被认知,尤其是从IPv4协议向IPv6协议移植时,由于IPv6地址比IPv4地址要长得多,使用名字服务显得更加正确和重要。
查找主机名最基本的函数是gethostbyname,如果调用成功,它返回一个指向结构hostent 的指针,该结构中包含了该主机的所有IPv4地址或IPv6地址。具体使用方法如下:#include
struct hostent *gethostbyname(const char *hostname);
此函数返回的非空指针指向下面的hostent结构:
struct hostent {
char *h_name;/*official name of host*/
char **h_aliases; /*pointer to array of pointers to alias names*/
int h_addrtype; /*host address type: AF_INET or AF_INET6*/
int h_length; /*length of address:4 or 16*/
char **h_addr_list; /*ptr to array of ptrs with IPv4 or IPv6 addrs*/
};
#define h_addr h_addr_list[0] /*first address in list*/
按照DNS,gethostbyname函数执行一个对A记录的查询或对AAAA记录的查询,它返回IPv4地址或IPv6地址。
图5-2所示为结构hostent和它所指向的各种信息的关系,该图中被查询的主机有两个别名和三个IPv4地址。在这些字段中,正式的主机名和所有的别名都是以空字符(“\0”)结尾的C字符串。
当返回IPv6地址时,结构hostent的成员h_addrtype被设置为AF_INET6,成员h_length 被设置为16。
从BIND 4.9.2版本开始,新的gethostbyname版本允许主机名参数是点分十进制数串,即下面调用是可行的:
hptr=gethostbyname(“202.198.16.3”);
gethostbyname函数与其它套接口函数的不同之处在于:当发生错误时,它不设置errno,而是将全局整数h_errno设置为定义在头文件
HOST_NOT_FOUND
●TRY_AGAIN
●NO_RECOVERY
●NO_DA TA (等同于NO_ADDRESS)
错误NO_DA TA表示指定的名字有效,但它既没有A记录,也没有AAAA记录。只有MX记录的主机名就是这样的例子。
BIND的当前版本提供函数hstrerror,它将h_errno的值作为唯一的参数,返回一个指向相应错误说明的const char *型指针。
下面程序给出了一个调用gethostbyname函数的例子,它可有任意数目的命令行参数,输出所有返回的信息。
#include "unp.h"
int
main(int argc, char **argv)
{
char *ptr, **pptr;
char str[INET6_ADDRSTRLEN];
struct hostent *hptr;
while (--argc > 0) {
ptr = *++argv;
if ( (hptr = gethostbyname(ptr)) == NULL) {
err_msg("gethostbyname error for host: %s: %s",
ptr, hstrerror(h_errno));
continue;
}
printf("official hostname: %s\n", hptr->h_name);
for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)
printf("\talias: %s\n", *pptr);
switch (hptr->h_addrtype) {
case AF_INET:
#ifdef AF_INET6
case AF_INET6:
#endif
pptr = hptr->h_addr_list;
for ( ; *pptr != NULL; pptr++)
printf("\taddress: %s\n",
Inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
break;
default:
err_ret("unknown address type");
break;
}
}
exit(0);
}
5.3 RES_USE_INET6解析器选项
BIND的较新版本(4.9.4及其以后版本)提供了一个名为RES_USE_INET6的解析器选项,用户可以用三种不同的方法来设置它。用户可以用此选项来通知解析器想让gethostbyname返回IPv6地址而不是IPv4地址。具体方法如下:
1.应用程序本身可以设置此选项,首先调用解析器的res_init函数,然后打开该选项:#include
res_init();
_res.options |= RES_USE_INT6;
上面语句必须在第一次调用gethostbyname或gethostbyname2之前完成。此选项仅对那些设置了此选项的应用程序才有效。
2.如果环境变量RES_OPTIONS含有串inet6,则此选项打开。此选项的作用依赖于环境变量的范围。例如,如果用户在.profile文件(使用Korn Shell)中以exports属性
设置它,例如:
export RES_OPTIONS=inet6
则它对从登录shell开始运行的每个程序都有效。但如果用户仅在命令行上设置该环境变量,则它仅对那个命令有影响。
3.解析器配置文件(一般为/etc/resolv.conf)可以包含如下行:
options inet6
在解析器配置文件中设置此选项影响主机上调用解析器函数的所有应用程序,因此,这项技术要直到结构hostent中返回的IPv6地址可以被主机上的所有应用程序所处理时才能使用。
第一种方法以每个应用程序为基础设置此选项,第二种方法以每个用户为基础,第三种方法以整个系统为基础。
当IPv6支持增加到BIND 4.9.4时,函数gethostbyname2也增加进去,它有两个参数允许用户指定地址族。具体使用方法如下:
#include
struct hostent * gethostbyname2(const char *hostname, int family);
该函数成功时返回非空指针,出错时返回空指针并设置h_errno值。该函数的返回值与gethostbyname的返回值相同,为一个指向结构hostent的指针,且此结构也保持不变。该函数的逻辑依赖于参数family和解析器选项RES_USE_INET6。
对于新选项RES_USE_INET6,函数gethostbyname和gethostbyname2的操作:
●RES_USE_INET6选项是否打开;
●gethostbyname2的第二个参数是AF_INET还是AF_INET6;
●解析器是搜索A记录还是搜索AAAA记录;
●返回地址长度是4还是16。
函数gethostbyname2的操作如下:
●如果参数family是AF_INET,则查询A记录。若不成功,则返回一个空
指针,若成功,则返回地址的类型和大小依赖于新的解析器选项
RES_USE_INET6:若选项未设置(缺省),则返回IPv4地址,结构hostent
的成员h_length的值将为4;若选项设置,则返回IPv4映射的IPv6地址,
结构hostent的成员h_length的值将为16。
●如果参数family为AF_INET6,则查询AAAA记录。若成功,则返回IPv6
地址,结构hostent的成员h_length的值将为16;否则返回一个空指针。
表5-1详细地总结了对于新选项RES_USE_INET6,函数gethostbyname和gethostbyname2的操作。
表5-1 解析器选项RES_USE_INET6与函数gethostbyname和gethostbyname2
如果应用程序想强制某个指定地址类型的搜索:IPv4或IPv6,则可以使用gethostbyname2函数。但对应用程序来说,调用gethostbyname函数似乎更常见,而且该函数的较新版本既可以返回IPv4地址,也可以返回IPv6地址。
如果解析器没有被初始化(没有设置标志RES_INIT),则调用res_init。此初始化函数检查并处理环境变量RES_OPTIONS。如果这个变量包含串inet6或如果解析器配置文件包含行options inet6,则标志RES_USE_INET6由res_init设置。res_init一般由函数gethostbyname或gethostbyaddr在第一次被应用程序调用时自动调用的。此外,应用程序也可以调用res_init,然后显式设置标志RES_USE_INET6。
总之,当选项RES_USE_INET6打开且应用程序调用gethostbyname时,应用程序通知
解析器:返回IPv6地址,首先搜索AAAA记录,如果未找到则搜索A记录,如果A记录找到则返回IPv4映射的IPv6地址。
5.4 与名字和地址有关的常用函数
5.4.1 gethostbyaddr函数
函数gethostbyaddr取一个二进制的IP地址并试图找到相应的主机名,此函数与gethostbyname的功能相反。
gethostbyaddr返回一个指向结构hostent的指针。具体使用方法如下:
#include
struct hostent *gethostbyaddr(const char *addr, size_t len, int family);
参数addr是一个真正指向含有IPv4或IPv6地址的结构in_addr或in6_addr的指针;len 是此结构的大小:对于IPv4地址为4,对于IPv6地址为16;参数family或者是AF_INET 或者是AF_INET6。
按照DNS原理,gethostbyaddr在域in_addr.arpa中给IPv4地址在名字服务员上查询PTR 记录,或在域ip6.int中给IPv6地址查询PTR记录。
gethostbyaddr总有一个地址族参数,所以当加上IPv6支持到BIND时,无需另一个函数(类似于函数gethostbyname2)。但是,当参数是IPv6地址时,仍有一些差别。下面的判断按步骤进行:
1.如果family是AF_INET6,len是16,且地址是IPv4映射的IPv6地址。则在域
in_addr.arpa中查找地址的低32位(IP地址部分)。
2.如果family是AF_INET6,len是16,且地址是IPv4兼容的IPv6地址。则在域
in_addr.arpa中查找地址的低32位(IP地址部分)。
3.如果被查找的是IPv4地址(或参数family为AF_INET,或上述两种情况中的一个
为真)且解析器选项RES_USE_INET6设置,则返回的地址(参数addr的一个拷贝)被转换为一个IPv4映射的IPv6地址:h_addrtype为AF_INET6,h_length为16。
5.4.2 uname函数
函数uname返回当前主机的名字。它虽然不是解析器库中的一部分,但它经常与函数gethostbyname一起用来确定本地主机的IP地址。具体使用方法如下:
#include
int uname(struct ustname *name);
该函数调用成功时返回一个非负整数,发生错误时返回-1。此函数装填结构utsname,其地址由调用者传递:
#idefine UTS_NAMESIZE 16
#define UTS_NODESIZE 256
struct utsname {
char sysname [_UTS_NAMESIZE]; /*name of this operating systen*/
char nodename[_UTS_NODESIZE]; /*name of this node*/
char release[_UTS_NAMESIZE]; /*OS release level*/
char version[_UTS_NAMESIZE]; /*OS version level*/
char machine[_UTS_NAMESIZE]; /*hardware type*/
这里需要注意的是,Posix.1所规定的只是上面的五个结构成员的名字以及每个数组是一个以空字符(“\0”)终止的字符数组,对于每个数组的大小及内容并未作任何说明。上面给出的大小来源于4.4BSD,其它操作系统采用不同的大小。
从网络程序设计角度来看,最严重的忽略是对数组nodename大小和内容的定义。有些系统仅在此数组中存储主机名(例如Jida),而另外一些系统存储FQDN(例如)。在有些操作系统如Solaris2.x上,既可以存放主机名,也可以存放FQDN,主要取决于管理员是如何安装操作系统的。
为了确定本地主机的IP地址,用户可以调用uname函数以得到主机名字,然后调用gethostbyname函数以得到它的所有IP地址。下面代码说明了这些步骤。
#include "unp.h"
#include
char **
my_addrs(int *addrtype)
{
struct hostent *hptr;
struct utsname myname;
if (uname(&myname) < 0)
return(NULL);
if ( (hptr = gethostbyname(myname.nodename)) == NULL)
return(NULL);
*addrtype = hptr->h_addrtype;
return(hptr->h_addr_list);
}
上述函数返回值是结构hostent的成员h_addr_list,即指向IP地址的指针数组。确定本地主机IP地址的另一种方法是ioctl的命令SIOCGIFCONF。
5.4.3 gethostname函数
函数gethostname的功能也是返回当前主机的名字。具体使用方法如下:
#include
int gethostname(char *name, size_t namelen);
name是指向主机名存储位置的在指针,namelen是此数组的大小。如果有空间,主机名以空字符结束。主机名的最大值通常是由头文件
从历史上看,uname由系统V定义,而gethostbyname由Berkeley定义。Posix.1 指定uname,但Unix 98两者都支持。
5.4.4 getservbyname和getservbyport函数
服务器也常常由名字来标识。如果在代码中,通过服务器的名字而不是通过服务器端口
号来认知它,而且如果从主机到端口号的映射包含在一个文件中(通常是/etc/services),则如果端口号改变,用户所需要做的修改就是改动/etc/services文件中的一行,而不需要重新编译应用程序。getservbyname函数可以根据给定的名字查找相应的服务,具体使用方法如下:
#include
struct servent * getservbyname(const char *servname, const char *protoname);
该函数调用出错时返回空指针,成功时返回非空指针,返回一个指向下面所示结构的指针:
struct servent {
char * s_name; /*official service name*/
char ** s_aliases; /*alias list*/
int s_port; /*port number, network-byte order*/
char * s_proto; /*protocol to use*/
};
服务器名字servname必须指定,如果还指定了一个协议,则结果表项也必须有匹配的协议。有些因特网服务既可使用UDP协议,又可以使用TCP协议,例如NFS(网络文件系统);而其它一些服务则仅支持单个协议。如果protoname没有指定且支持多个协议,则返回哪个端口是依赖于具体实现的。一般来说,支持多个协议的服务常常使用相同的TCP和UDP端口号。
结构servent中主要成员是端口号。由于端口号是以网络字节顺序返回的。在将它存储于套接口地址结构时,绝对不能调用htons函数。
对此函数的典型调用是:
struct servent *sptr;
sptr= getservbyname(“domain”, “udp”); /*DNS using UDP*/
sptr= getservbyname(“ftp”, “tcp”); /*FTP using TCP*/
sptr= getservbyname(“ftp”, NULL); /*FTP using TCP*/
sptr= getservbyname(“ftp”, “udp”); /*this call will fail*/
由于FTP仅支持TCP,所以第二个调用和第三个调用的结果是相同的,第四个调用将失败。下面是关于文件/etc/services中的内容:
ftp 21/tcp
telnet 23/tcp
tftp 69/udp
login 513/tcp
函数getservbyport在给定断口号和可选协议后查找相应的服务。具体使用方法如下:#include
struct servent * getservbyport(int port, const char *protoname);
port值必须为网络字节顺序。对此函数的典型调用是:
struct servent *sptr;
sptr=getservbyport(htons(53), “udp”); /*DNS using UDP*/
sptr=getservbyport(htons(21), “tcp”); /*FTP using TCP*/
sptr=getservbyport(htons(21), NULL); /*FTP using TCP*/
sptr=getservbyport(htons(21), “udp”); /*this call will fail*/
对于UDP,由于没有服务使用端口21,所以最后一个调用将失败。这里用户需要清楚,有些端口对于TCP可能用于一种服务,但对于UDP,同样的端口号也完全可能用于不同的
服务。
下面代码是使用gethostbyname和getservbyname函数实现TCP时间/日期顾客程序的例子。
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd, n;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
struct in_addr **pptr;
struct hostent *hp;
struct servent *sp;
if (argc != 3)
err_quit("usage: daytimetcpcli1
if ( (hp = gethostbyname(argv[1])) == NULL)
err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno));
if ( (sp = getservbyname(argv[2], "tcp")) == NULL)
err_quit("getservbyname error for %s", argv[2]);
pptr = (struct in_addr **) hp->h_addr_list;
for ( ; *pptr != NULL; pptr++) {
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = sp->s_port;
memcpy(&servaddr.sin_addr, *pptr, sizeof(struct in_addr));
printf("trying %s\n",
Sock_ntop((SA *) &servaddr, sizeof(servaddr)));
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) == 0)
break; /* success */
err_ret("connect error");
close(sockfd);
}
if (*pptr == NULL)
err_quit("unable to connect");
while ( (n = Read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
exit(0);
}
5.4.5 其它网络相关信息
本章的重点在于主机名与IP地址、服务名与端口号。其实应用程序可能需要查询和网络相关的四种类型信息:主机、网络、协议与服务。大多数查询都是针对主机的(gethostbyname和gethostbyaddr),有一小部分是针对网络服务的(getservbyname和getservbyport),针对网络和协议的查询就更少了。
所有四类信息都可以存储在文件中,而且每类信息都定义三个函数:
1.函数getXXXent读文件中的下一表项,在必要时可以打开文件。
2.函数setXXXent打开(如果文件没有打开)并回绕文件。
3.函数endXXXent关闭文件。
每类信息都定义了自己的结构,这些定义包含在
除了三个用于文件的顺序处理的get、set和end函数外,每类信息还提供了一些键值查询(keyed lookup)函数。它们顺序浏览文件(调用函数getXXXent来读每一行),但不返回每一行给调用者,而是查找一个与某参数匹配的表项。这些键值查询函数的名字类似函数getXXXbyYYY。例如,针对主机信息的两个关键字查询函数是gethostbyname(查找与主机名匹配的表项)和gethostbyaddr(查找与IP地址匹配的表项)。表5-2对此作了总结。
表5-2 四类与网络相关的信息
当DNS正在使用时,只有主机和网络信息是通过DNS提供的,服务和协议信息一般要从相应的文件中读。
还有一种名字和地址的转换方法是直接调用解析器函数,而不使用gethostbyname和gethostbyaddr。用这种方法来调用DNS的一个程序是sendmail,它搜索MX记录,这是gethostbyXXX函数无法做到的。解析器函数都有以res_开头的名字。
5.5 套接字选项
网络系统是通过核心的套接字结构来实现对传输层的抽象,系统为用户提供了获取和修改套接字结构中一些属性的函数,通过修改这些属性,用户可以调整套接字的性能,进而调整用户编写的网络应用的性能。有很多方法可以用来获取和设置套接口的选项,主要有:
●函数getsockopt和setsockopt
●函数fcntl
函数ioctl
5.5.1 获取和设置套接口选项
系统提供了函数getsockopt和setsockopt,分别用于获取套接口选项和设置套接口选项,这两个函数仅用于套接口。具体使用方法如下:
#include
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t *optlen);
sockfd是一个已经打开的套接口描述符,level是选项的层次,它指定系统中解释选项的代码:普通套接口代码或者特定于协议的代码,即通用套接口(SOL_SOCKET)、IP层套接口(IPPROTO_IP)和TCP套接口选项(IPPROTO_TCP)等层次。通用套接口选项一般可适用于TCP/UDP套接口,而TCP套接口选项只适用于TCP套接口。
optname是选项的名字。optval是一个指向变量的指针,用于存放获取或者设置的选项值的空间,此变量的大小由最后一个参数指定。由于不同的选项的数据类型不同,因此使用的是void *类型的指针。
表5-3总结了可由getsockopt获取或者由setsockopt设置的一些选项。表中数据类型列给出了指针optval必须指向的每个选项的数据类型
表5-3 套接口选项
有两种基本类型的套接口选项:打开或者关闭某个特性的二进制选项(标志),取得或者返回用户可以设置或检查的特定值的选项。标有“标志”的列指明选项是否为标志选项。当给这些标志选项调用函数getsockopt时,optval是一个整数。optval中返回的值是0表示选项关闭,非0表示选项打开。类似地,函数setsockopt要求一个非0的optval来打开选项,要求用0来关闭选项。如果“标志”不含有“*”,则选项用来在用户进程与系统间传递指定数据类型的值。
5.5.2 通用套接口选项
基本套接口选项是协议无关的,即它们由内核中的协议无关代码处理,而不是由诸如IPv6这样的一类特殊的协议模块来处理。但是这些选项仅能应用到某些确定类型的套接口中。例如,尽管SO_BROADCAST套接口选项是一个基本选项,但它仅能应用于数据报套接口。
SO_BROADCAST
此选项使能或者禁止套接口发送广播消息。在网络通信中,如果一个进程需要发送广播消息,必须满足两个条件:硬件必须支持广播,例如以太网;必须使用UDP数据报来发送广播消息,TCP套接口不能发送广播消息。
由于一个应用程序发送一个广播数据报之前必须设置此套接口,因此它能有效防止该进程在应用程序未设计成能广播时就发送广播消息。
套接口的缺省是禁止广播信息的发送。如果UDP程序希望发送广播消息,则可以使用下面语句修改设置。
int b_broadcast_on=1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST,&b_broadcast_on, sizeof(int));
SO_DEBUG
此选项只能使用在TCP套接口上,如果一个TCP套接口设置了这个选项,则系统将保存TCP发送和接收的所有数据的相关信息,供程序调试使用。这些信息保存在内核的环形缓冲区中,可由程序trpt来进行检查。
SO_DONTROUTE
此选项用于指定发送数据报时,不使用路由表来寻路。一般的IP包发送,路由守护进程routed需要查找路由表来为IP包选择发送的路由。如果用户设置了这个选项,这个套接口上的IP 数据报的发送将不使用路由表。
此选项经常有路由守护进程routed和gated用来旁路路由表(路由表不正确的情况下),强制一个分组从某个特定接口发出。
此时,系统将比较目的地址和主机的各个网络接口,如果目的地址的子网地址和网络接口的子网地址相同,则发送到那个网络接口。如果所有的网络接口都不和目的地址匹配,则发送函数将返回错误。
SO_ERROR
这是一个可以获取但不能设置的套接口选项。当套接口上发生错误时,源自Berkeley 的内核中的协议模块将此套接口的名为so_error的变量设置为标准的Unix Exxx值中的一个,它称为此套接口的待处理错误(pending error)。内核可以立即用下面两种方式通知进程:1.如果进程阻塞于对此套接口的select调用,则无论是检查可读条件还是可写条件,select均返回并设置其中一个或所有两个条件。
2.如果进程使用信号驱动I/O模型,则给进程或者进程组生成信号SIGIO。
进程后来可以通过获取SO_ERROR套接口选项来得到so_error的值。由getsockopt返回的整数值就是此套接口的待处理错误。so_error随后由内核复位为0。
当进程调用read且没有数据返回时,如果so_error为非0时,则read返回-1且errno 设置为so_error的值,接着so_error的值被复位为0。如果此套接口上有数据在排队,则read 返回那些数据而不是返回错误条件。如果在进程调用write时so_error为非0值,则write 返回-1且errno设置为so_error的值,so_error也被复位为0。
SO_KEEPALIVE
给一个TCP套接口设置保持存活(keepalive)选项后,如果用户2小时内在套接口的任意方向都没有数据交换,TCP就自动给对方发送一个保持存活探测分节(keepalive probe)。这是一个对方必须响应的TCP分节,它会导致以下三种情况:
1.对方以期望的ACK响应。由于TCP连接正常,应用程序得不到通知。又过仍无通
信的2小时后,TCP将发出另一个探测分节。
2.对方以RST响应,它告诉本地TCP,对方已经崩溃且已重新启动。套接口的待处
理错误为ECONNRESET,套接口本身则被关闭。
3.对方对保持存活探测分节无任何响应。源自Berkeley实现的TCP将发送另外8个
探测分节。相隔75秒发送一个,试图得到对方的响应。TCP在发送第一个探测分
节11分钟15秒后若仍无响应就放弃。如果对TCP的所有探测分节都没有响应,套
接口的待处理错误被设置为ETIMEOUT,套接口本身则被关闭。但如果套接口接
收到一个ICMP错误作为某个探测分节的响应,则返回相应的错误,套接口也被关
闭。此情况下一个常见的ICMP错误是“host unreachable”,说明对方主机并没有崩
溃,但是不可达。这种情况下待处理错误被设置为EHOSTUNREACH。
这个选项的目的是检测对方主机是否崩溃,尽管顾客也可以使用这个选项,此选项一般
由服务器使用。服务器使用此选项是因为它们花费大部分时间阻塞于等待跨TCP连接的输入上,即等待顾客请求。但如果顾客主机崩溃,服务器进程将永远不会知道,并将继续等待不会到达的输入,这种状态称为半开通连接(half open connection)。保持存活选项将检测出这些半开通连接并终止它们。
大多数Rlogin和Telnet服务器设置此选项,可以在交互式顾客未注销就强行退出系统(例如关闭终端电源)的情况下也能终止连接。
有些网络服务器,例如FTP服务器或WWW服务器,提供一个分钟数量级的超时。这是由应用进程本身完成的,这个超时与此套接口选项无关。
SO_LINGER
一般情况下,close系统调用立即返回,而后如果套接口缓冲区中还有数据没有发送完,则TCP将继续发送后续数据,在数据全部发送完后,TCP将开始断开连接的过程。但是如果用户修改了SO_LINGER的设置,则可以改变缺省的关闭过程。在修改此选项时要求在用户进程与内核间传递如下结构:
#include
struct linger {
int l_onoff; /*0=off, nonzero=on*/
int l_linger; /*linger time, Posix.1g specifies units as seconds*/
};
参数l_onoff用于设置关闭时是否延迟,参数l_linger用于设置延迟的时间。对setsockopt 的调用将依这两个结构成员的值导致下列三种情况之一:
1.参数l_onoff设置为0,选项关闭,这是缺省设置。参数l_linger的值被忽略且close
立即返回。
2.参数l_onoff为非0值且l_linger的值为0。此时,进程调用close时,TCP将立即
放弃这个连接,并将套接口中的是剩余数据丢弃,而后向对方发送RST数据段,接
着关闭这个套接口,释放套接口使用的资源。在这种情况下,数据将丢失,并且套
接口将不会进入TIME_W AIT状态,所以可能会出现数据混乱的问题。
3.参数l_onoff和l_linger都为非0值。此时进程调用close,close将延迟l_linger指定
的时间后返回。如果套接口缓冲区中还有剩余数据没有发送,则TCP将继续发送这
些数据,进程将一直在close系统调用中阻塞,直到数据全部发送成功,或者超时
时间后TCP放弃剩余数据。如果close返回时,数据全部发送成功,则close正常
返回;如果还没有全部发送,则close将错误返回,错误类型为EWOULDBLOCK。
此后本端TCP将以RST数据段回应对端发送来的数据段。
SO_OOBINLINE
此选项用于指示将带外数据看成普通数据,存放在正常的输入队列中,而不是将带外数据存放在单独的缓冲区中。当这种情况发生时,接收函数的MSG_OOB标志不能用来读带外数据。
SO_RCVBUF和SO_SNDBUF
每个套接口都有一个接收缓冲区和一个发送缓冲区,这两个选项分别用于设置套接口的接收和发送缓冲区的大小。对于不同的实现,缺省值的大小可以有很大的差别。较早期的源自Berkeley的实现将TCP发送和接收缓冲区大小均缺省为4096字节,但较新的系统使用了较大的值,可以是8192到61440字节间的任意值。如果主机支持NFS,
则UDP发送缓冲区的大小缺省为9000字节左右的一个值,而UDP接收缓冲区的大小缺省为40000字节左右的一个值。
当设置TCP套接口接收缓冲区的大小时,函数调用的顺序是很重要的,这是因为TCP的窗口规模选项是在建立连接时用SYN与对方互换得到的。对于一个顾客,这意味着SO_RCVBUF选项必须在调用connect之前设置;对于一个服务器,这意味着在调用listen之前必须给监听套接口设置这个选项。给已连接套接口设置这个选项对可能的窗口规模选项无任何影响,因为accept要直到TCP的三次握手完成才会创建并返回已连接套接口。
TCP套接口缓冲区的大小至少必须是连接的MSS的三倍。如果用户涉及到单向数据传输,例如单方向的文件传输,通常所说的套接口缓冲区的大小是指发送主机端的套接口发送缓冲区大小和接收主机端的套接口接收缓冲区大小。对于双向数据传输,在发送端指两个套接口缓冲区的大小,在接收端也指两个套接口缓冲区的大小。典型的缓冲区大小缺省值是8192字节或者更大,典型的MSS为512或1460字节,因此TCP套接口缓冲区大小的要求一般总能满足。但在大MTU的网络上会出现问题,因为它提供一个比通常MSS更大的值。
TCP套接口缓冲区大小还必须是连接的MSS的偶数倍。有些实现给应用程序处理这个细节问题,在连接建立后向上舍入套接口缓冲区大小。例如,使用缺省的4.4BSD 大小8192,并假设以太网的MSS为1460,在连接建立时两个套接口缓冲区向上舍入成8760(6*1460)。
在设置套接口缓冲区大小时另一个需要考虑的问题是性能。如果缓冲区设置太大,将会造成系统资源的浪费。对于套接口缓冲区,系统一般规定了一个上限,只有超级用户才能改变这个上限值。
如果缓冲区设置太小,对于TCP,将可能会引起数据重传次数的增加,影响传输的效率。因为一个TCP数据段到达对端TCP时,如果对端TCP套接口的缓冲区满,则这个数据段将被丢弃,发送端超时后将重发这个数据段。这种大量的重传实际上是对网络资源的极大浪费。对于UDP,如果套接口缓冲区太小,则将会导致数据报的大量丢失。
设置缓冲区大小的一般原则是,应当使接收端缓冲区能够对发送端的突发发送速率具有一定的容限。所谓突发就是发送速率可能在一段较短的时间内会大大地超出平均发送速率。可以用下面的公式粗略地计算缓冲区的大小:
BUF_SIZE = 最大突发时间* (突发的平均速率—接收平均速率)
最大突发时间是突发可能持续的最大时间长度。突发平均速率是在突发时间内速率的平均值。所以确定接收缓冲区的大小,需要考虑发送速率的性质和接收速率两个主要因素。在实际应用中,测量发送速率的变化可能比较困难,但是可以使用上面公式做一个粗略的估计,然后使用实验进一步调整。
SO_RCVLOW AT和SO_SNDLOW AT
每个套接口都有一个接收下限标记值和一个发送下限标记值。它们是函数select使用的,即当缓冲区的数据字节数超过SO_RCVLOW AT,则select将把那个套接口设置为读就绪;当缓冲区的数据字节数超过SO_SNDLOWAT,则select将把那个套接口设置为写就绪。
对于一个TCP或者UDP套接口SO_RCVLOWAT的缺省值是1;对于TCP套接口,SO_SNDLOWAT的缺省值是2048,但由于对于UDP套接口来说,发送缓冲区中可用空间的大小是不变的(因为UDP不保留由应用进程发送的数据报的拷贝),只要UDP套接口发
送缓冲区大小大于SO_SNDLOWAT,这样的UDP套接口总是可写的。
Posix.1g不要求支持这两个套接口。
SO_RCVTIMEO和SO_SNDTIMEO
这两个选项使得用户可以给套接口设置一个接收和发送超时时间,主要针对read/write、readv/writev、recv/send、recvfrom/sendto和recvmsg/sendmsg 5对函数使用,当这些函数在指定的时间内没有返回,则系统将强迫它们返回,错误为ETIMEOUT。可以使用下列语句设置超时选项。
struct timeval time_out;
time__sec=1;
time_out.tc_usec=0;
setsockopt(sock_fd, SO_RCVTIMEO, &time_out, sizeof(struct timeval));
SO_REUSEADDR和SO_REUSEPORT
SO_REUSEADDR套接口选项使用情况比较复杂,它主要为以下四种不同的目的提供服务:
1.SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知的端口,即使以前建立的将此端口用作它们的本地端口的连接仍然存在。这个条件通常是这样遇到的:
(a)启动一个监听服务器;
(b)连接请求到达,派生一个子进程来处理这个顾客请求。
(c)监听服务器终止,但子进程继续为现有连接上的顾客提供服务;
(d)重起监听服务器。
缺省时,当监听服务器通过调用socket、bind和listen重新启动时,因为它试图捆
绑一个现有连接上的端口(正由以前派生的子进程所处理),所以调用bind失败。
但若服务器在socket和bind调用间设置SO_REUSEADDR套接口选项,bind将成
功。所有TCP服务器应指定此套接口选项以允许服务器在这种情况下被重新启动。
2.SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的IP地址即可。这对用IP别名技术来提供多个HTTP服务器站点来说
是很常见的。例如,本地主机的主IP地址为202.198.16.1,但它有两个别名:
202.198.16.3和202.198.16.6。启动三个HTTP服务器,第一个HTTP服务器以本地
IP地址202.198.16.3和本地端口号80调用bind;第二个HTTP服务器捆绑
202.198.16.6和端口80,但它的bind调用将失败,除非在调用前设置
SO_REUSEADDR选项;第三个HTTP服务器以通配地址作为本地IP地址和端口
号80调用bind,同样设置SO_REUSEADDR选项是这个调用成功的关键。如果设
置了SO_REUSEADDR套接口选项,从而三个服务器都正常启动,目的IP地址为
202.198.16.3、目的端口号为80的外来TCP连接请求将递送给第一个服务器,目的
IP地址为202.198.16.6、目的端口号为80的外来TCP连接请求将递送给第二个服
务器,目的端口号为80的所有其它请求都递送给第三个服务器。最后一个服务器
处理目的地址为202.198.16.1或该主机已配置的其它任何IP别名的请求。
对于TCP,用户根本不能启动捆绑相同IP地址和相同端口号的多个服务器,这是
完全重复的捆绑。也就是说,即使用户为第二个服务器设置了SO_REUSEADDR选
项,也不能在启动绑定202.198.16.3和端口80的服务器后,接着再启动捆绑
202.198.16.3和端口80的另一个服务器。
3.SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定
不同的本地IP地址即可。在不支持SO_REUSEADDR套接口选项的系统上,这对
于要求知道顾客请求的IP地址的UDP服务器来说是非常普遍的。这项技术一般不
用于TCP服务器,因为TCP服务器在建立连接后总能通过调用getsockname来确
定顾客请求的目的IP地址。
4.SO_REUSEADDR允许完全重复的捆绑,即当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅
在支持多播的系统上才有,这些系统可能还不支持SO_REUSEADDR套接口选项,而且仅对UDP套接口有效(TCP不支持多播)。
此特性用于多播时,允许同一个主机上多次运行相同的应用程序。当一个UDP数
据报需由这些重复捆绑套接口中的一个接收时,所用的规则为:如果数据报的目的
地址是一个广播地址或多播地址,就给每个匹配的套接口递送数据报的拷贝。但是
如果数据报的目的地址是一个单播地址,那它只递送到单个套接口。在单播数据报
情况下,如果有多个套接口匹配数据报,则哪个套接口接收是依赖于实现的。
添加多播支持后,4.4BSD引入SO_REUSEPORT套接口选项。该选项与多播中完全重复捆绑的SO_REUSEADDR不同,该套接口选项具有以下语义:
1.此选项允许完全重复捆绑,但仅在每个想捆绑相同IP地址和端口的套接口都指定了此套接口选项才行。
2.如果被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT 等效。
目前,并非所有的系统都支持这个套接口选项,在那些不支持此选项但支持多播的系统上,使用SO_REUSEADDR而不是SO_REUSEPORT以允许重复捆绑。
通过上述讨论,用户进行网络程序设计时,应注意以下几点:
1.在所有TCP服务器程序中,在调用bind之前设置SO_REUSEADDR套接口选项。
2.在编写一个同一时刻在同一主机上可运行多次的多播应用程序时,设置SO_REUSEADDR套接口选项,并将本组的多播地址作为本地IP地址捆绑。
SO_TYPE
此套接口选项返回套接口的类型,返回的整数是一个诸如SOCK_STREAM或SOCK_DGRAM这样的值。此选项由启动时继承了套接口的进程所用。
SO_USELOOPBACK
此选项仅用于路由域(AF_ROUTE)的套接口,它对这些套接口的缺省设置为打开(它是唯一一个缺省为打开的SO_xxx套接口选项)。当此选项打开时,套接口接收在其上发送的任何数据的一个拷贝。当此选项关闭时,套接口禁止这些回馈拷贝;禁止这些回馈拷贝的另一种方法是调用shutdown,第二个参数设置为SHUT_RD。
5.5.3 IPv4套接口选项
IPv4套接口选项由IPv4处理,它的级别为IPPROTO_IP。除了IP_RECVIF外,下面介绍的套接口均由Posix.1g定义。
IP_HDRINCL
如果此选项给一个原始IP套接口设置,则用户必须为所有发送到此原始套接口上的数据报构造自己的IP头部。一般情况下,内核为发送到原始套接口上的是数据报构造IP头部,
但也有一些应用程序(例如路由跟踪程序Traceroute)要构造自己的IP头部以取代IP可能放到其头部的某些字段的值。
当设置此选项时,用户将构造完整的IP头部,但是下列情况除外:
●IP总是计算并存储IP头部校验和;
●如果用户将IP标识字段设置为0,内核将设置此字段;
●如果源IP地址是IN_ADDR_ANY,IP将它设置为外出接口的主IP地址;
●如何设置IP选项依赖具体实现。有些实现取IP_OPTIONS套接口选项中设置的任何
IP选项,并将它们附加到用户所构造的头部中,而其它实现则要求用户亲自在头部指定任何期望的IP选项。
IP_OPTIONS
设置此选项允许用户在IPv4头部中设置IP选项,要求用户清楚IP头部中IP选项的格式信息。
IP_RECVDSTADDR
这个套接口选项导致所接收的UDP数据报的目的IP地址由函数recvmsg作为辅助数据返回。
IP_RECVIF
这个套接口选项导致所接收的UDP数据报的接口索引由函数recvmsg作为辅助数据返回。
IP_TOS
此选项使用户可以给TCP或UDP套接口在IP头部中设置服务类型字段。如果用户给此选项调用getsockopt,则放到外出IP数据报头部的TOS字段中的当前值(缺省为0)将返回。还没有方法从接收的IP数据报中取此值。
用户可以将TOS设置为表5-4所示常值中的一个,它们在头文件
表5-4 IPv4服务类型定义
例如,Telnet和Rlogin服务应指定IPTOS_LOWDELAY,而FTP服务的传送数据部分应指定为IPTOS_THROUGHPUT。
IP_TTL
使用此选项用户可以设置和获取系统用于某个给定套接口的缺省TTL值。例如,4.4BSD 对TCP和UDP套接口都使用缺省值64,对原始套接口则使用缺省值255。和TOS字段一样,调用getsockopt返回系统用于外出数据报的缺省TTL字段值,用户不能从接收到的数据报中得到此值。
5.5.4 ICMPv6套接口选项