概述
常常地,我们都会使用数值地址来表示主机,用数值端口号来标识服务器。然而出于许多理由,我们应该使用名字而不是数值:名字比较容易记住;数值地址可以变动而名字保持不变;随着IPv6上转移,数值地址变得相当长,手工键入数值地址更易出错。本节讲述在名字和数值地址间进行转换的函数:
- 主机名与IPv4地址之间进行转换:gethostbyname 和 gethostbyaddr
- 服务名字和端口号之间进行转换:getservbyname 和 getservbyport
- 用于主机名字和IP地址之间以及服务名字和端口号之间的转换:getaddrinfo 和 getnameinfo
域名系统(DNS)
域名系统(DNS)主要用于主机名字与IP之间的映射。
gethostbyname函数
作用:查找主机名
返回:若成功,则返回一个指向hostent结构的指针,该结构中含有所查找主机的所有IPv4地址
注意:仅仅返回IPv4的地址
#include <netdb.h>
struct hostent* gethostbyname(const char *hostname);
//返回:若成功则为非空指针,若出错则未NULL且设置h_errno
本函数返回的非空指针指向如下的hostent结构:
struct hostent{
char *h_name; //正式主机名,即所查询主机的规范名字
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
【注】gethostbyname与其他套接字函数不同之处在于:当发生错误时,它不设置errno变量,而是将全局整数变量h_errno设为在头文件<netdb.h>中定义的下列常值之一:
- HOST_NOT_FOUND
- TRY_AGAIN
- NO_RECOVERY
- NO_DATA(等同于NO_ADDRESS)
如今多数解析器提供名为hstrerror的函数,它以某个h_errno值作为唯一的参数,返回的是一个const char*指针,指向相应错误的声明。
【示例】为任意数目的命令行参数调用gethostbyname,并显示返回所有信息
#include "unp.h"
int main(int argc,char **argv){
char *ptr,**pptr;
char str[INET_ADDRSTRLEN];
struct hostent *hptr;
while(--argc > 0){
ptr = *++argv;
//8~14:给每个命令行参数调用gethostbyname
if((hptr = gethostbyname(ptr)) == NULL){
err_msg("gethostbyname eror for host: %s: %s",ptr,hstrerror(h_errno));
continue;
}
//14
//15~17:输出规范主机名,后跟别名列表
printf("official hostname: %s\n",hptr->h_name);
for(pptr = hptr->h_aliases;*pptr != NULL;pptr++)
printf("\talias: %s\n",*pptr);
//17
//18~24:pptr指向一个指针数组,其中每个指针指向一个地址。对于每一个地址,
// 我们调用inet_ntop并输出返回的字符串
switch(hptr->h_addrtype){
case AF_INET:
pptr = hptr->h_addr_list;
for( ; *pptr != NULL; pptr++)
printf("\taddress: %s\n",Inet_ntop(hptr->h_addrtype, \
*pptr,str,sizeof(str)));
break;
//24
default:
err_ret("unknown address type");
break;
}
}
}
gethostbyaddr函数
作用:试图由一个二进制的IP地址找到相应的主机名,与gethostbyname的行为刚好相反。
#include <netdb.h>
struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family);
//返回:若成功则为非空指针,若出错则为NULL且设置h_errno
getservbyname和getservbyport函数
像主机一样,服务也通常靠名字来认知。如果我们在程序代码中通过其名字而不是其端口号来指代一个服务,而且从名字到端口号的映射关系保存在一个文件中(通常是/etc/services),那么即使端口号发生变动,我们需修改的仅仅是/etc/services文件中的某一行,而不必重新编译应用程序。
getservbyname函数
作用:用于根据给定名字查找相应服务。
#include <netdb.h>
struct servent *getservbyname(const char *servname, const char *protoname);
//返回:若成功则为非空指针,若出错则为NULL
本函数返回的非空指针指向如下的servent结构:
struct servent{
char *s_name; //官方指定服务名称
char **s_aliases; //别名
int s_port; //端口号,网络字节序,存放时不能使用htons
char *s_proto; //使用的协议
};
本函数的典型调用如下:
struct servent *sptr;
sptr = getservbyname("domain", "udp");
sptr = getservbyname("ftp", "tcp");
sptr = getservbyname("ftp", NULL);
sptr = getservbyname("ftp", "udp"); //此调用失败
以下是/etc/services文件中典型的文本行:
getservbyport函数
作用:根据给定端口号和可选协议查找相应服务
#include <netdb.h>
struct servent *getservbyport(int port, const char* protoname);
//返回:若成功则为非空指针,若出错则为NULL
port参数的值必须为网络字节序,本函数的典型调用如下:
struct servent *sptr;
sptr = getservbyport(htons(53),"udp");
sptr = getservbyport(htons(21),"tcp");
sptr = getservbyport(htons(21),"NULL");
sptr = getservbyport(htons(21),"udp"); //没有UDP服务在端口21上,此调用失败
例子:使用gethostbyname和getservbyname,通过TCP时间获取客户程序,并且尝试连接到多宿服务器主机的每个IP地址,直到有一个连接成功或所有地址尝试完毕。
#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 = gethostbynmame(argv[1])) == NULL){
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函数
作用:能够处理名字到地址以及服务到端口这两种转换
返回:一个sockaddr结构而不是一个地址列表
#include <netdb.h>
int getaddrinfo(const char *hostname, const char *service,\
const struct addrinfo *hints, struct addrinfo **result);
//返回:若成功则为0,若出错则未非0
【参数解析】
- hostname:一个主机名或地址串(IPv4的点分十进制数串或IPv6的十六进制数串);
- service:一个服务名或十进制端口号数串;
- hints:可以是空指针,也可以是一个指向某个addrinfo结构的指针 ;
- 调用者在这个结构中填入关于期望返回的信息类型的暗示;
- 举例来说,如果指定服务及支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置为SOCK_DGRAM,使得返回的仅仅是适用于数据报套接字的信息;
本函数通过result指针参数返回一个指向addrinfo结构链表的指针,而addrinfo结构定义在头文件<netdb.h>中:
struct addrinfo{
int ai_flags; //零个或多个或在一起的AI_XXX值
int ai_family; //某个AF_XXX值,如AF_INET
int ai_socktype; //某个SOCK_XXX值,如SOCK_STREAM
int ai_protocol;
socklen_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
};
其中ai_flags成员可用标志值及其含义如下:
AI_PASSIVE :套接字将用于被动打开
AI_CANONNAME :告知getaddrinfo函数返回主机的规范名字
AI_NUMERICHOST :防止任何类型的名字到地址映射,hostname参数必须是一个地址串
AI_NUMERICSERV :防止任何类型的名字到服务映射,service参数必须是一个十进制端口号数串
AI_V4MAPPED :如果同时制定ai_family成员的值为AF_INET6,那么如果没有可用的AAAA
记录,就返回与A记录对应的IPv4映射的IPv6地址
AI_ALL :如果同时指定AI_V4MAPPED标志,那么除了返回与AAAA记录对应的IPv6地址外,
还返回与A记录对应的IPv4映射的IPv6地址
AI_ADDRCONFIG :按照所在主机的配置选择返回地址类型,也就是只查找与所在主机回馈接口以外
的网络接口配置的IP地址版本一致的地址
gai_seterror函数
作用:以getaddrinfo返回的非0错误值的名字和含义为唯一参数,返回一个指向对应的出错信息串的指针。
#include <netdb.h>
const char* gai_strerror(int error);
//返回:指向错误描述消息字符串的指针
freeaddrinfo函数
作用:将getaddrinfo返回的所有动态获取的存储空间通过调用freeaddrinfo返回给系统,其中包括addrinfo结构、ai_addr结构和ai_canonname字符串。
#include <netdb.h>
void freeaddrinfo(struct addrinfo *ai);
host_serv函数
作用:访问getaddrinfo的第一个接口函数不要求调用者分配并填写一个hints结构
#include "unp.h"
struct addrinfo *host_serv(const char *hostname, const char *service,
int family, int socktype);
//返回:若成功则为指向addrinfo结构的指针,错出错则为NULL
如下是这个函数的源码:
#include "unp.h"
//该函数初始化一个hints结构,调用getaddrinfo,若出错则返回一个空指针。
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.socktype = socktype;
if((n = getaddrinfo(host,serv,&hints,&res))!=0)
return NULL;
return (res);
}
tcp_connect函数
作用:创建一个TCP套接字并连接到一个服务器(使用getaddrinfo函数)
#include "unp.h"
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;
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;
do{
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
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套接字,给它捆绑服务器的众所周知端口,并允许接收外来的连接请求
#include "unp.h"
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(struct addrinfo));
hints.ai_flags = AI_PASSIVE;
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);
}
udp_client函数
作用:用于创建未连接UDP套接字的udp_client函数
#include "unp.h"
int udp_client(const char *hostname, const char *service,
struct sockaddr **saptr, socklen_t *lenp);
//返回:若成功则为未连接套接字描述符,错出错则不返回
【参数解析】
- 返回值:套接字的描述符
- saptr:指向某个(由udp_client动态分配的)套接字地址结构的(由调用者自行声明)一个指针的地址,本函数把目的地址和端口存放在这个结构中,用于稍后调用sendto
- lenp:套接字地址结构的大小,不能是空指针,因为sendto和recvfrom调用都需要知道套接字地址结构的长度
#include "unp.h"
int udp_client(const char *host, const char *serv, SA **saptr, socklen_t *lenp)
{
int sockfd, n;
struct addrinfo hints,*res,*ressave;
bzero(&hints, sizeof(struct addrinfo));
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);
if(addrlenp)
*addrlenp = res->ai_addrlen;
*saptr = malloc(res->ai_addrlen);
memcpy(*saptr,res->ai_addr,res->ai_addrlen);
*lenp = res->ai_addrlen;
freeaddrinfo(ressave);
return (sockfd);
}
udp_connect函数
作用:创建一个已连接UDP套接字
#include "unp.h"
int udp_connect(const char *hostname, const char *service);
//返回:若成功则为已连接套接字描述符,若出错则不返回
#include "unp.h"
int udp_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;
hints.ai_socktype = SOCK_DGRAM;
if((n = getaddrinfo(host, serv, &hints, &res)) != 0)
err_quit("udp_connect 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(connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
break;
close(sockfd);
}while((res = res->ai_next)!=NULL)
if(res == NULL)
err_sys("udp_connect error for %s, %s", host, serv);
freeaddrinfo(ressave);
return (sockfd);
}
【注】本函数几乎等同于tcp_connect。两者的差别之一是UDP套接字上的connect调用不会发送任何东西到对端。如果存在错误(譬如对端不可达或所指定端口上没有服务器),调用者就得等到向对端发送一个数据报之后才能发现。
udp_server函数
作用:简化访问getaddrinfo
#include "unp.h"
int udp_server(const char *hostname, const char *service, socklen_t *lenptr);
//返回:若成功则为未连接套接字描述符,若出错则不返回
#include "unp.h"
int udp_server(const char *host, const char *serv, SA **saptr, socklen_t *addrlenp)
{
int sockfd, n;
struct addrinfo hints,*res,*ressave;
bzero(&hints, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
if((n = getaddrinfo(host, serv, &hints, &res)) != 0)
err_quit("udp_server 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;
close(sockfd);
}while((res = res->ai_next)!=NULL)
if(res == NULL)
err_sys("udp_server error for %s, %s", host, serv);
if(addrlenp)
*addrlenp = res->ai_addrlen;
freeaddrinfo(ressave);
return (sockfd);
}
【注】本函数除了没调用listen外,其余与tcp_listen相同。
getnameinfo函数
作用:该函数是getaddrinfo的互补函数,它以一个套接字地址为参数,返回描述其中的主机的一个字符串和描述其中的服务的另一个字符串。
#include <netdb.h>
int getnameinfo(const struct sockaddr *sockaddr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);
//返回:若成功则为0,若出错则为非0
【参数解析】
- sockaddr:指向一个套接字地址结构,其中包含待转换成直观可读的字符串的协议地址
- addrlen:这个结构的长度,常常由accept、recvfrom、getsockname或getpeername返回
【区别】
- sock_ntop和getnameinfo差别在于:
- 前者不涉及DNS,只返回IP地址和端口号一个可显示版本;
- 后者通常尝试获取主机和服务的名字;