程序人生 | unix 网络编程之 getaddrinfo 函数详解及使用举例

本文首发于 2015-01-03 21:04:36

概述

IPv4 中使用 gethostbyname() 函数完成主机名到地址解析,这个函数仅仅支持 IPv4 ,且不允许调用者指定所需地址类型的任何信息,返回的结构只包含了用于存储 IPv4 地址的空间。

IPv6 中引入了getaddrinfo()的新 API,它是协议无关的,既可用于 IPv4 也可用于 IPv6 。

getaddrinfo函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个addrinfo的结构(列表)指针而不是一个地址清单。这些addrinfo结构随后可由 socket 函数直接使用。

getaddrinfo函数把协议相关性安全隐藏在这个库函数内部。应用程序只要处理由 getaddrinfo 函数填写的套接口地址结构。该函数在 POSIX 规范中定义了。

函数说明

包含头文件:

1
2
3
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

函数原型:

1
int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result );

参数说明:

  • hostname: 一个主机名或者地址串(IPv4 的点分十进制串或者 IPv6 的 16 进制串)。
  • service:服务名可以是十进制的端口号,也可以是已定义的服务名称,如 ftp、http 等。
  • hints:可以是一个空指针,也可以是一个指向某个 addrinfo 结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。
  • result:本函数通过 result 指针参数返回一个指向 addrinfo 结构体链表的指针。

返回值:

0:成功;非 0:出错。

参数设置

getaddrinfo函数之前通常需要对以下 6 个参数进行以下设置:nodename、servname、hints的ai_flags、ai_family、ai_socktype、ai_protocol

在 6 项参数中,对函数影响最大的是nodename,sernamehints.ai_flag,而ai_family只是有地址为 v4 地址或 v6 地址的区别。ai_protocol一般为 0 不作改动。

getaddrinfo 在实际使用中的几种常用参数设置:

一般情况下,client/server 编程中,server 端调用bind(如果面向连接的还需要listen);client 则无需调用bind函数,解析地址后直接connect(面向连接)或直接发送数据(无连接)。因此,比较常见的情况有:

  1. 通常服务器端在调用getaddrinfo之前,ai_flags设置AI_PASSIVE,用于bind;主机名nodename通常会设置为 NULL,返回通配地址[::]
  2. 客户端调用getaddrinfo时,ai_flags一般不设置AI_PASSIVE,但是主机名nodename和服务名servname(更愿意称之为端口)则应该不为空。
  3. 当然,即使不设置AI_PASSIVE,取出的地址也并非不可以被 bind,很多程序中ai_flags直接设置为 0,即 3 个标志位都不设置,这种情况下只要 hostname 和 servname 设置的没有问题就可以正确 bind。

上述情况只是简单的 client/server 中的使用,但实际在使用 getaddrinfo 和查阅国外开源代码的时候,曾遇到一些将 servname(即端口)设为 NULL 的情况(当然,此时 nodename 必不为 NULL,否则调用 getaddrinfo 会报错)。

使用须知

1)如果本函数返回成功,那么由 result 参数指向的变量已被填入一个指针,它指向的是由其中的ai_next成员串联起来的addrinfo结构链表。

1
2
3
4
5
6
7
8
9
10
11
struct addrinfo
{        
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
struct sockaddr *ai_addr; /* 我认为这个成员是这个函数最大的便利。 */
char *ai_canonname;
struct addrinfo *ai_next;
};

其中,sockaddr 结构体为:

1
2
3
4
5
6
在linux环境下,结构体struct sockaddr在/usr/include/linux/socket.h中定义,具体如下:
typedef unsigned short sa_family_t;
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
}

而且,sockaddr一般要转换为sockaddr_in

1
2
3
4
5
6
7
struct sockaddr_in
{
  short int sin_family;
  unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}

2)可以导致返回多个 addrinfo 结构的情形有以下 2 个:

  1. 如果与 hostname 参数关联的地址有多个,那么适用于所请求地址簇的每个地址都返回一个对应的结构。
  2. 如果 service 参数指定的服务支持多个套接口类型,那么每个套接口类型都可能返回一个对应的结构,具体取决于 hints 结构的 ai_socktype 成员。

举例来说:如果指定的服务既支持 TCP 也支持 UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM,使得返回的仅仅是适用于数据报套接口的信息。

3)我们必须先分配一个 hints 结构,把它清零后填写需要的字段,再调用 getaddrinfo,然后遍历一个链表逐个尝试每个返回地址。

4)getaddrinfo 解决了把主机名和服务名转换成套接口地址结构的问题

5)如果 getaddrinfo 出错,那么返回一个非 0 的错误值。输出出错信息,不要用 perror,而应该用gai_strerror,该函数原型为:

1
const char *gai_strerror( int error );

该函数以getaddrinfo返回的非 0 错误值的名字和含义为他的唯一参数,返回一个指向对应的出错信息串的指针。

6)由 getaddrinfo 返回的所有存储空间都是动态获取的,这些存储空间必须通过调用freeaddrinfo返回给系统,该函数原型为:

1
void freeaddrinfo( struct addrinfo *ai );

ai参数应指向由getaddrinfo返回的第一个 addrinfo 结构。

这个链表中的所有结构以及它们指向的任何动态存储空间都被释放掉。

示例

1. 根据主机名获取 IP 地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
if (argc != 2) {
printf("Usag: ./a.out hostname|ip\n");
exit(1);
}
struct addrinfo hints;
struct addrinfo *res, *cur;
int ret;
struct sockaddr_in *addr;
char ipbuf[16];
int port;

memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET; /* Allow IPv4 */
hints.ai_flags = AI_PASSIVE; /* For wildcard IP address */
hints.ai_protocol = 0; /* Any protocol */
hints.ai_socktype = SOCK_DGRAM;
ret = getaddrinfo(argv[1], NULL,&hints,&res);
if (ret < 0) {
fprintf(stderr, "%s\n", gai_strerror(ret));
exit(1);
}

for (cur = res; cur != NULL; cur = cur->ai_next) {
addr = (struct sockaddr_in *)cur->ai_addr;
printf("ip: %s\n", inet_ntop(AF_INET, &addr->sin_addr, ipbuf, 16));
printf("port: %d\n", inet_ntop(AF_INET, &addr->sin_port, (void *)&port, 2));
//printf("port: %d\n", ntohs(addr->sin_port));

}
freeaddrinfo(res);
exit(0);
}

2. 根据主机名和端口号获取地址信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
#include <ctype.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <netdb.h>
#include <sys/socket.h>
void main()
{
struct addrinfo *ailist, *aip;
struct addrinfo hint;
struct sockaddr_in *sinp;
char *hostname = "localhost";
char buf[INET_ADDRSTRLEN];
char *server = "6543"; /* 这是服务端口号 */
const char *addr;
int ilRc;
hint.ai_family = AF_UNSPEC; /* hint 的限定设置 */
hint.ai_socktype = 0; /* 这里可是设置 socket type . 比如 SOCK——DGRAM */
hint.ai_flags = AI_PASSIVE; /* flags 的标志很多 。常用的有AI_CANONNAME; */
hint.ai_protocol = 0; /* 设置协议 一般为0,默认 */
hint.ai_addrlen = 0; /* 下面不可以设置,为0,或者为NULL */
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
ilRc = getaddrinfo(hostname, server, &hint, &ailist);
if (ilRc < 0)
{
printf("str_error = %s\n", gai_strerror(errno));
return;
}

/* 显示获取的信息 */
for (aip = ailist; aip != NULL; aip = aip->ai_next)
{
sinp = (struct sockaddr_in *)aip->ai_addr;
addr = inet_ntop(AF_INET, &sinp->sin_addr, buf, INET_ADDRSTRLEN);
printf(" addr = %s, port = %d\n", addr?addr:"unknow ", ntohs(sinp->sin_port));
}
}

3. 由内核分配随机端口(再也不担心端口被占了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

#define MAX_CONN_COUNT 10
#define INVALID_SOCKET (~0)

int main(int argc, char **argv)
{
int motionListenPort = 0;
int motion_sock = 0;
int err;
int maxconn;
char familyDesc[32];
struct sockaddr_storage motion_sock_addr;
socklen_t alen;
struct addrinfo *addrs = NULL, *addr, hints;
int ret;
int tries = 0;

memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET; /* Allow IPv4 */
hints.ai_flags = AI_PASSIVE; /* For wildcard IP address */
hints.ai_protocol = 0; /* Any protocol */
hints.ai_socktype = SOCK_STREAM;
ret = getaddrinfo(NULL, "0", &hints, &addrs);
if (ret < 0)
{
fprintf(stderr, "%s\n", gai_strerror(ret));
exit(1);
}

for (addr = addrs; addr != NULL; addr = addr->ai_next) {
/* Create the socket. */
if ((motion_sock = socket(addr->ai_family, SOCK_STREAM, 0)) == INVALID_SOCKET)
{
fprintf(stderr, "Error:could not create socket for the motion\n");
continue;
}

/* Bind it to a kernel assigned port on localhost and get the assigned port via getsockname(). */
if (bind(motion_sock, addr->ai_addr, addr->ai_addrlen) < 0)
{
fprintf(stderr, "Error: could not bind socket for the motion\n");
close(motion_sock);
motion_sock = INVALID_SOCKET;
continue;
}

alen = sizeof(motion_sock_addr);
if (getsockname(motion_sock, (struct sockaddr *) &(motion_sock_addr), &alen) < 0)
{
fprintf(stderr, "could not get address of socket for the motion\n");
close(motion_sock);
motion_sock = INVALID_SOCKET;
continue;
}

/* Resolve the motion listen port. */
switch(motion_sock_addr.ss_family)
{
case AF_INET:
{
struct sockaddr_in *motion_addr = (struct sockaddr_in *) &motion_sock_addr;
motionListenPort = ntohs(motion_addr->sin_port);
strcpy(familyDesc, "IPv4");
fprintf(stdout, "motionListenPort=%d, familyDesc = %s\n", motionListenPort, familyDesc);
break;
}
case AF_INET6:
{
struct sockaddr_in6 *motion_addr = (struct sockaddr_in6 *) &motion_sock_addr;
motionListenPort = ntohs(motion_addr->sin6_port);
strcpy(familyDesc, "IPv6");
fprintf(stdout, "motionListenPort=%d, familyDesc = %s\n", motionListenPort, familyDesc);
break;
}
default:
{
fprintf(stderr, "Error:unrecognized address family \"%d\" for the motion\n", motion_sock_addr.ss_family);
continue;
}
}

/* 监听 */
maxconn = MAX_CONN_COUNT;
err = listen(motion_sock, maxconn);
if (err < 0)
{
fprintf(stderr, "could not listen on socket for the motion\n");
close(motion_sock);
motion_sock = INVALID_SOCKET;
continue;
}
}

if (motion_sock == INVALID_SOCKET)
goto listen_failed;

/* XXXXXX socket通信过程 */


freeaddrinfo(addrs);
close(motion_sock);
return 0;

listen_failed:
fprintf(stderr, "Error: failed to listen for the motion\n");
if (addrs)
freeaddrinfo(addrs);

if (motion_sock != INVALID_SOCKET)
close(motion_sock);
motion_sock = INVALID_SOCKET;
return -1;
}

4. 使用 ioctl 获取指定网卡 IP 地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

#define ETH_NAME "eth0"

int main()

{
int sock;
struct sockaddr_in sin;
struct ifreq ifr;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock == -1)
{
perror("socket");
return -1;
}

strncpy(ifr.ifr_name, ETH_NAME, IFNAMSIZ);
ifr.ifr_name[IFNAMSIZ - 1] = 0;
if (ioctl(sock, SIOCGIFADDR, &ifr) < 0)
{
perror("ioctl");
return -1;
}

memcpy(&sin, &ifr.ifr_addr, sizeof(sin));
fprintf(stdout, "eth0: %s\n", inet_ntoa(sin.sin_addr));

return 0;
}

5. 使用 ping 指令,根据 hostname 获取 ip 地址

本例未用 getaddrinfo,而是采用 shell 指令方法(不推荐)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>


//使用ping指令,根据hostname获取ip地址
int getIpAddrByHostname(char *hostname, char* ip_addr, size_t ip_size)
{
char command[256];
FILE *f;
char *ip_pos;

snprintf(command, 256, "ping -c1 %s | head -n 1 | sed 's/^[^(]*(\\([^)]*\\).*$/\\1/'", hostname);
fprintf(stdout, "%s\n", command);
if ((f = popen(command, "r")) == NULL)
{
fprintf(stderr, "could not open the command, \"%s\", %s\n", command, strerror(errno));
return -1;
}

fgets(ip_addr, ip_size, f);
fclose(f);

ip_pos = ip_addr;
for (;*ip_pos && *ip_pos!= '\n'; ip_pos++);
*ip_pos = 0;

return 0;
}

int main()
{
char addr[64] = {0};
getIpAddrByHostname("localhost", addr, INET_ADDRSTRLEN);
fprintf(stdout, "localhost: %s\n", addr);
return 0;
}

欢迎关注我的微信公众号【数据库内核】:分享主流开源数据库和存储引擎相关技术。

欢迎关注公众号数据库内核
标题 网址
GitHub https://dbkernel.github.io
知乎 https://www.zhihu.com/people/dbkernel/posts
思否(SegmentFault) https://segmentfault.com/u/dbkernel
掘金 https://juejin.im/user/5e9d3ed251882538083fed1f/posts
CSDN https://blog.csdn.net/dbkernel
博客园(cnblogs) https://www.cnblogs.com/dbkernel
文章目录
  1. 1. 概述
  2. 2. 函数说明
  3. 3. 参数设置
  4. 4. 使用须知
  5. 5. 示例
    1. 5.1. 1. 根据主机名获取 IP 地址
    2. 5.2. 2. 根据主机名和端口号获取地址信息
    3. 5.3. 3. 由内核分配随机端口(再也不担心端口被占了)
    4. 5.4. 4. 使用 ioctl 获取指定网卡 IP 地址
    5. 5.5. 5. 使用 ping 指令,根据 hostname 获取 ip 地址
|