socket地址的两个要素,即IP地址和端口号,都是用数值表示。这其实不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。因此你会经常看见用主机名访问一台机器,而不是IP地址;用服务名称来代替端口号。
gethostbyname 和 gethostbyaddr
- gethostbyname 函数根据主机名称获取主机的完整信息
- gethostbyaddr 根据IP地址获取主机的完整信息
gethostbyname函数通常先在本地/etc/hosts配置文件中查找主机,
如果没有找到,再去访问DNS服务器。
这两个函数的定义如下:
#include
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
name参数指定目标主机的主机名,addr参数指定目标主机的IP地址,len参数指定addr所指IP地址的长度,type参数指定addr所指IP地址的类型,其合法取值包括AF_INET(用于IPv4地址)和AF_INET6(用于IPv6地址)。
这两个函数返回的都是hostent结构体类型的指针,host结构体的定义如下:
#include
struct hostent
{
char* h_name; /* 主机名 */
char** h_aliases; /* 主机别名列表,可能有多个 */
int h_addrtype; /* 地址类型(地址族)*/
int h_length; /* 地址长度*/
char** h_addr_list; /* 按网络字节序列列出的主机IP地址列表*/
};
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
struct hostent *info = gethostbyname("www.baidu.com");
if(!info){
printf("error: %d - %s\n", errno, strerror(errno));
}
//主机名
printf("%s\n", info->h_name);
//别名
for(int i=0; info->h_aliases[i]; i++){
printf("Aliases %d: %s\n", i+1, info->h_aliases[i]);
}
//地址类型
printf("Address type: %s\n", (info->h_addrtype==AF_INET) ? "AF_INET": "AF_INET6");
//地址长度
printf("Address length: %d\n", info->h_length);
//地址
if(info->h_addrtype == AF_INET){
char buf[INET_ADDRSTRLEN];
for(int i=0; info->h_addr_list[i]; i++){
inet_ntop(info->h_addrtype, info->h_addr_list[i], buf, INET_ADDRSTRLEN);
printf("IPv4 addr %d: %s\n", i+1, buf);
}
}
if(info->h_addrtype == AF_INET6){
char buf[INET6_ADDRSTRLEN];
for(int i=0; info->h_addr_list[i]; i++){
inet_ntop(info->h_addrtype, info->h_addr_list[i], buf, INET6_ADDRSTRLEN);
printf("IPv6 addr %d: %s\n", i+1, buf);
}
}
return 0;
}
getsrvbyname 和 getsrvbyport
- getservbyname函数根据名称获取某个服务的完整信息
- getservbyport函数根据端口号获取某个服务的完整信息
它们实际上都是通过读取/etc/services 文件获取服务的信息。
#include
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
name参数指定目标服务的名字,port参数指定目标服务对应的端口号。proto参数指定服务类型,给它传递“tcp”表示获取流服务,给它传递“udp"表示获取数据报服务;给它传递NULL则表示获取所有类型服务。
这两个函数返回的都是servent结构体类型的指针,结构体servent的定义如下:
#include
struct servent
{
char* s_name; /* 服务名称 */
char** s_aliases; /* 服务的别名列表, 可能有多个 */
int s_port; /* 端口号 */
char* s_proto; /* 服务类型, 通常是tcp或者udp */
};
下面是通过主机名和服务名来访问目标服务器上的daytime服务。
#include
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
assert(argc == 2);
const char *host = argv[1];
/*获取目标主机地址信息*/
struct hostent* hostinfo = gethostbyname(host);
assert(hostinfo);
/*获取daytime服务信息*/
struct servent *servinfo = getservbyname("daytime", "tcp");
assert(servinfo);
printf("daytime port is %d\n", ntohs(servinfo->s_port));
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int result = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
assert(result != -1);
char buffer[128];
result = read(sockfd, buffer, sizeof(buffer));
assert(result);
buffer[result] = '\0';
printf("the day time is: %s\n", buffer);
close(sockfd);
return 0;
}
这里需要指出的是,上面的4个函数都是不可重入的,即非线程安全的。不过netdb.h头文件给出了它们的可重入版本。正如Linux下所有其他函数的可重入版本的命名规则那样,这些函数的函数名是原函数名尾部加上_r(re-entrant)。
getaddinfo
getaddrinfo函数既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重入取决于其内部调用的gethostbyname和getservbyname函数是否是它们的可重入版本。该函数的定义如下:
#include
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
- hostname参数可以接受主机名,也可以接受字符串表示的IP地址(IPv4采用点分十进制字符串,IPv6则采用十六进制字符串)
- service参数可以接受服务名,也可以接收字符串表示的十进制端口号
- hints参数是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制,hints参数可以被设置为NULL,表示允许getaddrinfo反馈任何可用的结果。
- result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果。
getaddrinfo反馈的每一条结果都是addrinfo结构体类型的对象,结构体addrinfo的定义如下:
struct addrinfo
{
int ai_flags;
int ai_family; /* 地址族 */
int ai_socktype; /* 服务类型 SOCK_STREAM或SOCK_DGRAM*/
int ai_protocol;
socklen_t ai_addrlen; /* socket地址ai_addr的长度 */
char* ai_canonname; /* 主机的别名 */
struct sockaddr* ai_addr; /* 指向socket地址 */
struct addrinfo* ai_next;
};
- ai_protocol成员是指具体的网络协议,其含义和socket系统调用的第三个参数相同,它通常被设置为0。
- ai_flags可以按照下表的标志按位或
当我们使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。
下面是利用hints参数获取主机daytime流服务信息:
struct addrinfo hints;
struct addrinfo *res;
bzero(&hints, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("alexdev", "daytime", &hints, &res);
这里需要注意的是getaddrinfo隐式地分配了堆内存,因此在调用结束后,我们必须使用如下配对函数来释放这块内存
#include
void freeaddrinfo(struct addrinfo * res);
getnameinfo
getnameinfo函数通过socket地址同时获得以字符串表示的主机名(内部使用gethostbyname函数)和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和getservbyport函数是否是它们的可冲入版本。函数定义如下:
#include
int getnameinfo( const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
getnameinfo将返回的主机名存储在host参数指向的缓存中,将服务名称存储在serv参数指向的缓存中,hostlen和serlen参数分别指定这两块缓存的长度。flags参数控制getnameinfo的行为:
getaddrinfo和getnameinfo函数成功时返回0,失败则返回错误码,可能的错误码如下:
Linux下strerror函数能将数值错误码errno转换成易读的字符串形式。同样,下面的函数可以将上表中的错误码转换成其字符串形式:
#include
const char* gai_strerror(int error);