综述
这一章主要探讨的问题是我们如何实现主机名-ip地址,服务名-端口号之间的转换。
在IPv4的版本下,我们对应的有四个函数:1. 主机名到ip地址,gethostbyname 2. ip地址反查主机名, gethostbyaddr 3.服务名到端口号, getservbyname 4.端口号反查服务名, getservbyport
上述的四个函数虽然好用,但是问题在于不兼容IPv6,于是我们引入了getaddrinfo函数,它会返回一个协议无关的地址信息的链表。我们也介绍了gai_strerror函数,它可以读取getaddrinfo函数返回值来返回可读的错误原因。由于getaddrinfo采用了动态内存的方式,我们需要引入freeaddrinfo来避免内存泄漏。
getaddrinfo虽然很强大,但是使用起来比较繁琐,有很多固定的处理逻辑。于是我们从不同的应用场景引入了五个对应的接口,我们用host_serv可以快速的基于主机名和服务名获取相对应的addrinfo而不用考虑释放、错误处理等。我们可以用tcp_connect,udp_client,udp_connect三个函数,来实现基于给定主机名字(ip)和服务名字(端口)来分别完成tcp连接,udp未连接套接字和udp已连接套接字。我们也可以用tcp_listen和udp_server来完成连接,其目的在于可以基于服务名字,完成协议无关的服务器绑定。
我们然后提到了和getaddrinfo相对应的getnameinfo,它可以基于给定的套接字地址(包含了ip地址和端口号),反查出主机名字和服务名字。
最后我们提到了可重入函数的概念,不可重入的函数意味着如果信号处理函数和主控制流都调用了同一函数,那么存在着被覆盖重写的危险。我们指出ipv4的四个函数式不可重入的,而inet_ntoa也是不可重入的,inet_ntop和inet_pton是可重入的,getaddrinfo和getnameinfo则是可重入的。我们指出errno虽然不是什么函数,但是由于其是进程独有(而不是线程独有),所以也有同样的风险。对于不可重入函数,我们建议在信号处理函数中避免调用,对于errno覆盖问题,我们建议采用事先保存,事后恢复值的办法
域名系统
资源记录
DNS中的条目被称之为资源记录(RR),我们感兴趣的主要就几种
名称 | 示例 | 作用 |
---|---|---|
A | freebsd in A 12.106.32.254 |
将主机名字映射成IPv4地址 |
AAAA | freebsd in AAAA 3ffe:b80:1f8d:1:a00:20ff:fea7:686b |
将主机名字映射成IPv6地址 |
PTR | 将ip地址映射成主机名 | |
MX | 将主机作为指定主机的”邮件交换器“ | |
CNAME | ftp in cname linux.unpbook.com |
规范名字,为常见的服务(如ftp或者www)指定cname记录,这样即便该服务被挪动了主机,仍然能够找到 |
解析器和名字服务器
我们是通过调用解析器的函数库中的函数来接触DNS服务器的,常见的解析器函数包括gethostbyname和gethostbyaddr
解析器代码会读取系统配置文件来确定本组织结构的名字服务器的所在位置(ip地址),/etc/resolv.conf中通常存储了本地名字服务器主机的IP地址。然后用UDP向本地名字服务器发送查询,如果本地不知道,那么本地会去查询其他名字服务器,如果消息太长,那么就会转成TCP
DNS替代方法
- 静态主机文件,就是/etc/hosts/文件
- 网络消息系统NIS
- 轻量级目录访问协议LDAP
对于 开发者来说,这些是透明的,我们只需要调用解析器函数如gethostname,gethostbyaddr
gethostbyname函数
函数定义
struct hostent *gethostbyname(const char *);
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses from name server */
};
解释
gethostbyname执行的是对A记录的查询,只能返回IPv4地址
h_name表示的是规范名字,比方说ftp.unpbook.com的规范名字是linux.unpbook.com
与之前的函数不太一样的是,如果发生了错误,不会设置errno变量,而是设置h_errno变量为以下之一:HOST_NOT_FOUND,TRY_AGAIN,NO_RECOVERY,NO_DATA(相当于NO_ADDRESS)。
期中,NO_DATA表示名字有效,但是没有A记录(比方说有MX记录)
一般来说,我们可以用hstrerror函数来得到错误说明
代码实例
char *ptr, **pptr;
char str[INET_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:
pptr=hptr->h_addr_list;//得到的是ipv4地址
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;
}
}
gethostbyaddr函数
简述
gethostbyaddr试图通过二进制的IP地址找到相应的主机名字
函数定义
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int family);
解释
一般来说,addr参数是一个指向in_addr结构的指针,len是该结构的大小(IPv4是4),family参数是AF_INET
通常来说,我们感兴趣的是主机名字,所以可以看返回值的h_name
getservbyname和getservbyport函数
简述
一般来说,服务 也是依赖于名称识别,这么做的优势在于,一旦主机发生了改变,我们只需要修改特定文件(一般是/etc/services),而不需要编译
函数定义
struct servent *getservbyname(const char *servname, const char *protoname);
struct servent *getservbyport(int port, const char *protoname);
struct servent {
char *s_name; /* official service name */
char **s_aliases; /* alias list */
int s_port; /* port # */
char *s_proto; /* protocol to use */
};
解释
getservbyname
对于getservbyname来说,servname是必须的,protoname可以不指定,如果指定的话,那么就要求该服务必须有匹配的协议,如果不指定协议而服务支持多个协议,那么返回的端口就取决于实现(一般没所谓,因为tcp和udp端口号一般一致,但是没有保证)
servent中我们主要关注的是端口号(s_port),注意返回的是网络字节序,所以不要在其上调用htons等
常用调用类似下面
struct servent *sptr;
sptr=getservbyname("domain","udp");
getservbyport
port传入的时候必须要是网络字节序
常规调用类似下面
struct servent *sptr;
sptr=getservbyport(htons(53),"udp");
代码示例
#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 in_addr *inetaddrp[2];
struct in_addr inetaddr;
struct hostent *hp;
struct servent *sp;
if (argc!=3)
err_quit("usage:daytimetcpcli1 <hostname> <service>");
if ( (hp=gethostbyname(argv[1]))==NULL){//如果发现传入的第一个参数无法通过gethostbyname获取,那么就假设是传入了ip地址继续尝试
if (inet_aton(argv[1],&inetaddr)==0){
err_quit("hostname error for %s: %s",argv[1],hstrerror(h_errno));
}else{
inetaddrp[0]=&inetaddr;
inetaddrp[1]=NULL;
pptr=inetaddrp;
}
}else{
pptr=(struct in_addr **)hp->h_addr_list;
}
if ( (sp=getservbyname(argv[2],"tcp"))==NULL){//获取服务对应的端口号
err_quit("getservbyname error for %s",argv[2]);
}
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;
err_ret("connect error");
close(sockfd);
}
if (*pptr==NULL)
err_quit("unable to connect");
while ( (n=Read(sockfd,recvline,MAXLINE))>0){
recvline[n]=0;
Fputs(recvline,stdout);
}
exit(0);
}
getaddrinfo函数
简述
gethostbyname和gethostbyaddr只支持IPv4,而getaddrinfo可以同时实现基于名字找地址和找端口,并且对外返回sockaddr结构而隐藏了协议相关性
函数定义
int getaddrinfo(const char * hostname, const char * service,
const struct addrinfo * hints,
struct addrinfo ** result);
struct addrinfo {
int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
int ai_family; /* PF_xxx */
int ai_socktype; /* SOCK_xxx */
int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
socklen_t ai_addrlen; /* length of ai_addr */
char *ai_canonname; /* canonical name for hostname */
struct sockaddr *ai_addr; /* binary address */
struct addrinfo *ai_next; /* next structure in linked list */
};
解释
传入参数
hostname是主机名字或者地址串,service参数是服务名字或者十进制端口号数串,hints可以是空指针,用于填入期望返回的信息类型的暗示。比方说如果及支持TCP又支持UDP,那么通过设置ai_socktype为SOCK_DGRAM,我们可以只返回适用于UDP的信息
一般来说我们可以在hints中配置的主要是四项:ai_flags,ai_family,ai_socktype和ai_protocol
对于ai_flags我们可用的标志值和含义如下
标志值 | 含义 |
---|---|
AI_PASSIVE | 套接字将用于被动打开 |
AI_CANONNAME | 告知返回规范名字 |
AI_NUMERICHOST | 防止任何类型的名字到地址的映射,hostname必须是一个地址串 |
AI_NUMERICSERV | 防止任何类型的名字到服务的映射,service必须是一个十进制端口号数串 |
AI_V4MAPPED | 如果同时指定了ai_family成员为AF_INET6,那么如果没有可用的AAAA记录,返回与A记录对应的IPv4映射的IPv6地址 |
AI_ALL | 如果同时指定了AI_V4MAPPED,那么除了返回与AAAA记录对应的IPv6地址外,还返回与IPv4映射的IPv6地址 |
AI_ADDRCONFIG | 按照所在主机的配置选择返回地址类型,就是只查找与所在主机回馈接口以外的网络接口配置的IP地址版本一致的地址 |
如果hints为空指针,那么就会假设ai_flags,ai_socktype和ai_protocol的值为0,ai_family的值为AF_UNSPEC
返回值
如果该函数返回成功(返回0),那么result指向的变量已经被填充,指向的是由ai_next串起来的addrinfo链表
之所以会返回多个结构,有可能有两种情形
- hostname相关联的地址有多个,那么就会对期中所有适用于所请求地址族(ai_family)的地址都返回一个
- service参数指定的服务是用于多个套接字类型,那么就可能返回多个结构(对应不同的套接字类型),具体还要取决于ai_socktype成员
链表的顺序没有任何保证,不能假定tcp在前或者在后
当我们得到addrinfo结构之后,我们可以直接利用其来调用套接字函数,如ai_family,ai_socktype和ai_addr可以用于socket函数,ai_addr和ai_addrlen可以用于connect或者bind的第二个或者第二三个参数
如果我们设置了AI_CANONNAME,那么函数返回的第一个addrinfo结构的ai_cononname成员指向了规范名字
hints常见的输入
- 指定hostname和service
- 指定service而不指定hostname,同时配置AI_PASSIVE。一般用于服务器,配置后返回的addrinfo中含有一个值为INADDR_ANY(IPv4)或者IN6ADDR_ANY_INIT(ipv6)的ip地址,然后服务器调用socket,bind和listen
gai_strerror函数
简述
该函数主要用于解读getaddrinfo返回的非0错误值的解读
函数定义
const char *gai_strerror(int error);
freeaddrinfo函数
简述
由于getaddrinfo返回的存储空间都是动态获取的,我们需要调用freeaddrinfo来释放返回
函数定义
void freeaddrinfo(struct addrinfo *ai);
解释
该函数传入的ai应该是链表的第一个结构,链表所有的结构以及指向的动态空间都会被释放(复制的时候需要注意)
host_serv函数
简述
该函数是我们自行定义的接口函数,应用于当我们没有兴趣自行书写hints
函数定义和源代码
struct addrinfo *host_serv(const char *host, const char *serv, int family, int socktype)
{
int n;
struct addrinfo hints,*res;
bzero(&hints,sizeof(struct addrinfo));
hints.ai_flags=AI_CANONNAME;
hints.ai_family=family;
hints.ai_socktype=socktype;
if ( (n=getaddrinfo(host,serv,&hints,&res))!=0)
return NULL;
return(res);
}
tcp_connect函数
简述
该函数也是我们自行定义的接口函数,我们可以用于直接tcp连接
函数定义和源代码
int tcp_connect(const char *host, const char *serv)
{
int sockfd, n;
struct addrinfo hints, *res, *ressave;
bzero(&hints, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;//表示IPv4和IPv6都可以
hints.ai_socktype = SOCK_STREAM;
if ( (n=getaddrinfo(host,serv,&hints,&res))!=0)//如果无法解析,那就退出
err_quit("tcp_connect error for %s, %s: %s",host,serv,gai_strerror(n));
ressave=res;//用于保存链表头部(方便free)
do{
sockfd=socket(res->ai_family,res->ai_socktype,res->ai_protocol);//注意对于addrinfo信息的利用
if (sockfd<0)
continue;//因为可能还有其他地址可以尝试
if (connect(sockfd,res->ai_addr,res->ai_addrlen)==0)
break;//成功的话不需要再尝试
Close(sockfd);
}while( (res=res->ai_next)!=NULL);//尝试所有
if (res==NULL)//表示所有都试过且失败了
err_sys("tcp_connect error for %s,%s",host,serv);
freeaddrinfo(ressave);
return(sockfd);
}
tcp_listen函数
简述
该函数可以执行tcp服务器的通常步骤,就是捆绑端口并接受外来连接请求
比起直接连接,其优势在于协议无关(可以同时处理IPv6)
源代码
int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
int listenfd,n;
const int on=1;
struct addrinfo hints,*res,*ressave;
bzero(&hints,sizeof(hints));
hints.ai_flags=AI_PASSIVE;//如果绑定通配地址(通常如此),那么这两个配置(AF_UNSPEC)一般会导致传出两个地址,分别是IPv4和IPv6
hints.ai_family=AF_UNSPEC;
hints.ai_socktype=SOCK_STREAM;
if ( (n=getaddrinfo(host,serv,&hints,&res))!=0)
err_quit("tcp_listen error for %s, %s:%s",host,serv,gai_strerror(n));
ressave=res;
do{
listenfd=socket(res->ai_family,res->ai_socktype,res->ai_protocol);
if (listenfd<0)
continue;
Setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
if (bind(listenfd,res->ai_addr,res->ai_addrlen)==0)
break;
Close(listenfd);
}while( (res=res->ai_next)!= NULL);
if (res==NULL)
err_sys("tcp_listen error for %s,%s",host,serv);
Listen(listenfd,LISTENQ);
if (addrlenp)
*addrlenp=res->ai_addrlen;
freeaddrinfo(ressave);
return listenfd;
}
解释
在调用的时候,我们一般可以设置第一个参数(指定主机)和第三个参数(长度)为NULL
在上述源代码中我们提到了,在通配情况下,调用会导致说getaddrinfo会返回IPv6(如果我们只是希望绑定在IPv4下),一个简单的技巧就是
udp_client函数
简述
该函数是我们自定义的接口,特别的,udp_client将会用于创建未连接udp套接字
函数定义和源代码
int udp_client(const char *host, const char *serv,
struct sockaddr **saptr, socklen_t *lenp)
{
int sockfd,n;
struct addrinfo hints,*res,*ressave;
//常规套路
bzero(&hints,sizeof(hints));
hints.ai_family=AF_UNSPEC;
hints.ai_socktype=SOCK_DGRAM;
if ( (n=getaddrinfo(host,serv,&hints,&res))!=0)
err_quit("udp_client error for %s, %s:%s",host,serv,gai_strerror(n));
ressave=res;
do{
sockfd=socket(res->ai_family,res->ai_socktype,res->ai_protocol);
if (sockfd>=0)
break;
}while( (res=res->ai_next)!= NULL);
if (res==NULL)
err_sys("udp_client error for %s, %s",host,serv);
//之所以下面要这么做(而不能直接指向res对应的地方),是因为free之后动态内存就被回收了
*saptr=Malloc(res->ai_addrlen);
memcpy(*saptr,res->ai_addr,res->ai_addrlen);
*lenp=res->ai_addrlen;
freeaddrinfo(ressave);
return(sockfd);
}
解释
其实从上面可以看出来,核心思路就是,配置hints,然后调用getaddrinfo,然后去尝试连接什么的(协助)
udp_connect函数
简述
该函数也是用于udp,但是是用来构建已连接udp套接字的
定义与源代码解释
int udp_connect(const char *host, const char *serv)
{
int sockfd,n;
struct addrinfo hints,*res,*ressave;
bzero(&hints,sizeof(hints));
hints.ai_family=AF_UNSPEC;
hints.ai_socktype=SOCK_DGRAM;
if ( (n=getaddrinfo(host,serv,&hints,&res))!=0)
err_quit("udp_client error for %s, %s:%s",host,serv,gai_strerror(n));
ressave=res;
do{
sockfd=socket(res->ai_family,res->ai_socktype,res->ai_protocol);
//下面是与client不同的地方,首先这里的逻辑增加了conenct调用
if (sockfd<0)
continue;
if (connect(sockfd,res->ai_addr,res->ai_addrlen)==0)
break;
Close(sockfd);
}while( (res=res->ai_next)!= NULL);
if (res==NULL)
err_sys("udp_client error for %s, %s",host,serv);
//注意这里不需要复制了,因为已连接udp套接字可以直接使用
freeaddrinfo(ressave);
return(sockfd);
}
udp_server函数
简述
该接口函数式为了建立一个udp服务器,与tcp_listen相同,hostname是可选的
定义和源代码
int udp_server(const char *host, const char *serv, socklen_t *addrlenp)
{
int sockfd, n;
struct addrinfo hints, *res, *ressave;
bzero(&hints, sizeof(hints));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
err_quit("udp_client error for %s, %s:%s", host, serv, gai_strerror(n));
ressave = res;
do
{
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sockfd < 0)
continue;
if (bind(sockfd, res->ai_addr, res->ai_addrlen) == 0)
break;
//区别在于这里不用调用listen
Close(sockfd);
} while ((res = res->ai_next) != NULL);
if (res == NULL)
err_sys("tcp_listen error for %s,%s", host, serv);
if (addrlenp)
*addrlenp=res->ai_addrlen;
freeaddrinfo(ressave);
return sockfd;
}
getnameinfo函数
简述
是getaddrinfo的互补函数,是以套接字地址为参数,返回描述其中主机的一个字符串和描述其中服务的另一个字符串,该函数是协议无关的
函数定义
int getnameinfo(const struct sockaddr * sockaddr, socklen_t addrlen,
char * host, socklen_t hostlen, char * serv,
socklen_t servlen, int flags);
解释
如果用户不想要获取主机或者端口名字,仅仅需要设置相应的len为0
sock_ntop和getnameinfo的区别在于,前者不涉及dns,只是返回可显示版本;后者则尝试获取主机和服务端名字
flags有以下六个标志
常值 | 说明 |
---|---|
NI_DGRAM | 数据报服务 |
NI_NAMEREQD | 若不能从地址解析出名字则返回错误 |
NI_NOPQDN | 只返回FQDN的主机名部分 |
NI_NUMERICHOST | 用数串的形式返回主机字符串 |
NI_NUMERICSCOPE | 以数串的形式返回范围标识字符串 |
NI_NUMERICSERV | 以数串的形式返回服务字符串 |
如果我们知道是数据报套接字,我们就需要设置NI_DGRAM了,因为套接字地址结构中给出的仅仅是IP地址和端口号,所以getnameinfo无法确定所用的协议(TCP或者UDP),而一些端口号在TCP和UDP上的服务是不一致的,比如说514在tcp上是rsh服务,在udp上则是syslog服务
如果无法使用DNS反向解析出主机名字,设置了NI_NAMEREQD标志会导致返回一个错误
NI_NOFQDN会导致返回主机名第一个点号后面的内容被删去,比方说不设置的话返回aix.unpbook.com,设置的话返回aix
NI_NUMERICHOST告知不要调用dns(因为会耗时),而是以字符串的方式返回IP地址(内部可能是用inet_ntop实现)。NI_NUMERICSERV会返回端口号而避免查找服务名;NI_NUMERICSCOPE会以数值返回范围标识来代替名字。由于客户的端口号一般没有关联的服务名(是临时的),所以一般服务器应该设置NI_NUMERICSERV标志
可重入函数
gethostbyname和gethostbyaddr不是可重入的,这由他们使用static存储导致。
不可重入意味着,如果主控制流和信号处理函数都调用了gethostbyname或者gethostbyaddr。那么假设说当控制流正在执行gethostbyname并且已经填写好host变量准备返回的时候,信号发出,信号处理函数被调用,它也会调用gethostbyname,最后主控制流中的值就被重写了
总的来说,gethostbyname,gethosybyaddr,getservbyname,getservbyport四个函数都是不可重入的。而inet_pton和inet_ntop是重入的,但是inet_ntoa是不可重入的。getaddrinfo和getnameinfo由于自身的处理,所以是可重入的
errno变量的话,是每个进程都有一个副本,如果是多线程情况下,那么也可能发生问题
通常来说,解决不可重入函数的问题的办法,就是不在信号处理函数中调用任何不可重入函数。对于errno问题,我们可以将信号处理函数设置成事先保存,事后恢复值的办法,如下图所示
void sig_alrm(int signo){
int errno_save;
errno_save=errno;//存储
if (write(...)!=nbytes)
fprintf("xx");//错误处理
errno=errno_save;//恢复
}
厂商有提供另外的gethostbyname_r和getservbyname_r方法来解决这种不可重入的问题。