4.17 域名解析 API 介绍
为了便于记忆,有时候我们需要我们的程序可以使用域名和端口号去连接服务,这种情况下,我们需要使用 socket API gethostbyname 函数先把域名转换成 ip 地址,再使用 connect 函数连接。在 Linux 系统上, gethostbyname 函数的签名如下:
#include <netdb.h>
struct hostent* gethostbyname(const char* name);
2
3
域名转换成 ip 时,转换结果存在一个 hostent 结构体中。转换成功后的 ip 地址存放在 hostent 最后一个字段中,hostent 结构体类型定义如下:
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 */
}
#define h_addr h_addr_list[0] /* for backward compatibility */
2
3
4
5
6
7
8
9
10
- 字段 h_name: 地址的正式名称;
- 字段 h_aliases: 地址的预备名称指针;
- 字段 h_addrtype: 地址类型,通常是AF_INET;
- 字段 h_length: 地址的长度,以字节数目为计量单位;
- 字段 h_addr_list:主机网络地址指针,网络字节顺序。 其中,h_addr 是字段 h_addr_list 中的第一地址。
注意:虽然 h_addr_list[0] 看起来是一个 char* 类型,但实际上是一个 uint32_t,这是 ip 地址的 32 bit 整数表示形式,如果需要转换成十进制点分法字符串再调用 inet_ntoa() 函数即可。
我们来看一段示例代码:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
//extern int h_errno;
bool connect_to_server(const char* server, short port)
{
int hSocket = socket(AF_INET, SOCK_STREAM, 0);
if (hSocket == -1)
return false;
struct sockaddr_in addrSrv = { 0 };
struct hostent* pHostent = NULL;
//unsigned int addr = 0;
//如果传入的参数 server 的值是 somesite.com 这种域名域名形式则 if 条件成立,
//接着调用 gethostbyname 解析域名为 4 字节的 ip 地址(整型)
if (addrSrv.sin_addr.s_addr = inet_addr(server) == INADDR_NONE)
{
pHostent = gethostbyname(server);
if (pHostent == NULL)
return false;
//当使用 gethostbyname 解析域名时可能会得到多个 ip 地址,一般最常用的使用第一个 ip 地址
addrSrv.sin_addr.s_addr = *((unsigned long*)pHostent->h_addr_list[0]);
}
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(port);
int ret = connect(hSocket, (struct sockaddr*)&addrSrv, sizeof(addrSrv));
if (ret == -1)
return false;
return true;
}
int main()
{
if (connect_to_server("baidu.com", 80))
printf("connect successfully.\n");
else
printf("connect error.\n");
return 0;
}
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
上述 connect_to_server 函数既可以支持直接传入域名,也可以传入 ip 地址:
connect_to_server("127.0.0.1", 8888);
connect_to_server("localhost", 8888);
connect_to_server("61.135.169.125", 80);
connect_to_server("baidu.com", 80);
2
3
4
5
实际在使用 gethostbyname 函数时需要注意以下:
gethostbyname 函数是不可重入函数,在 Linux 下建议使用 gethostbyname_r 函数替代;
gethostbyname 在解析域名时,会阻塞当前执行线程的,直到得到返回结果;
在使用 gethostbyname 函数出错时,你不能使用 errno 获取错误码信息(因此也不能使用 perror() 函数打印错误信息),你应该使用 h_errno 错误码(也可以调用 herror() 打印错误信息),herror() 函数签名如下:
void herror(const char *s);
1
在新的 Linux 系统中,gethostbyname 和 gethostbyaddr 一样,已经被标记为废弃的,你应该使用新的函数 getaddrinfo 去替代它们,getaddrinfo 签名如下:
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char* node,
const char* service,
const struct addrinfo* hints,
struct addrinfo** res);
2
3
4
5
6
7
8
getaddrinfo 函数调用成功返回 0,失败返回非 0 值,调用成功后结果存储在参数 res 中。addrinfo 结构体定义如下:
struct addrinfo
{
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr* ai_addr;
char* ai_canonname;
struct addrinfo* ai_next;
};
2
3
4
5
6
7
8
9
10
11
如果你不再需要 res 这个变量,记得使用 freeaddrinfo 函数将其指向的资源释放:
void freeaddrinfo(struct addrinfo* res);
getaddrinfo 使用示例如下:
struct addrinfo hints = {0};
hints.ai_flags = AI_CANONNAME;
hints.ai_family = family;
hints.ai_socktype = socktype;
struct addrinfo* res;
int n = getaddrinfo(host, service, &hints, &res);
if(n == 0)
{
//调用成功,使用 res
//释放 res 资源
freeaddr(res);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
getaddrinfo 函数不仅支持 ipv4,同时也支持 ipv6 的解析,redis-server 的源码中就使用了这个函数去解析 ipv4 和 ipv6 的地址(位于 net.c 文件中):
static int _redisContextConnectTcp(redisContext *c, const char *addr, int port,
const struct timeval *timeout,
const char *source_addr) {
int s, rv, n;
char _port[6]; /* strlen("65535"); */
struct addrinfo hints, *servinfo, *bservinfo, *p, *b;
int blocking = (c->flags & REDIS_BLOCK);
int reuseaddr = (c->flags & REDIS_REUSEADDR);
int reuses = 0;
long timeout_msec = -1;
servinfo = NULL;
c->connection_type = REDIS_CONN_TCP;
c->tcp.port = port;
/* We need to take possession of the passed parameters
* to make them reusable for a reconnect.
* We also carefully check we don't free data we already own,
* as in the case of the reconnect method.
*
* This is a bit ugly, but atleast it works and doesn't leak memory.
**/
if (c->tcp.host != addr) {
if (c->tcp.host)
free(c->tcp.host);
c->tcp.host = strdup(addr);
}
if (timeout) {
if (c->timeout != timeout) {
if (c->timeout == NULL)
c->timeout = malloc(sizeof(struct timeval));
memcpy(c->timeout, timeout, sizeof(struct timeval));
}
} else {
if (c->timeout)
free(c->timeout);
c->timeout = NULL;
}
if (redisContextTimeoutMsec(c, &timeout_msec) != REDIS_OK) {
__redisSetError(c, REDIS_ERR_IO, "Invalid timeout specified");
goto error;
}
if (source_addr == NULL) {
free(c->tcp.source_addr);
c->tcp.source_addr = NULL;
} else if (c->tcp.source_addr != source_addr) {
free(c->tcp.source_addr);
c->tcp.source_addr = strdup(source_addr);
}
snprintf(_port, 6, "%d", port);
memset(&hints,0,sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
/* Try with IPv6 if no IPv4 address was found. We do it in this order since
* in a Redis client you can't afford to test if you have IPv6 connectivity
* as this would add latency to every connect. Otherwise a more sensible
* route could be: Use IPv6 if both addresses are available and there is IPv6
* connectivity. */
if ((rv = getaddrinfo(c->tcp.host,_port,&hints,&servinfo)) != 0) {
hints.ai_family = AF_INET6;
if ((rv = getaddrinfo(addr,_port,&hints,&servinfo)) != 0) {
__redisSetError(c,REDIS_ERR_OTHER,gai_strerror(rv));
return REDIS_ERR;
}
}
for (p = servinfo; p != NULL; p = p->ai_next) {
addrretry:
if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1)
continue;
c->fd = s;
if (redisSetBlocking(c,0) != REDIS_OK)
goto error;
if (c->tcp.source_addr) {
int bound = 0;
/* Using getaddrinfo saves us from self-determining IPv4 vs IPv6 */
if ((rv = getaddrinfo(c->tcp.source_addr, NULL, &hints, &bservinfo)) != 0) {
char buf[128];
snprintf(buf,sizeof(buf),"Can't get addr: %s",gai_strerror(rv));
__redisSetError(c,REDIS_ERR_OTHER,buf);
goto error;
}
if (reuseaddr) {
n = 1;
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*) &n,
sizeof(n)) < 0) {
goto error;
}
}
for (b = bservinfo; b != NULL; b = b->ai_next) {
if (bind(s,b->ai_addr,b->ai_addrlen) != -1) {
bound = 1;
break;
}
}
freeaddrinfo(bservinfo);
if (!bound) {
char buf[128];
snprintf(buf,sizeof(buf),"Can't bind socket: %s",strerror(errno));
__redisSetError(c,REDIS_ERR_OTHER,buf);
goto error;
}
}
if (connect(s,p->ai_addr,p->ai_addrlen) == -1) {
if (errno == EHOSTUNREACH) {
redisContextCloseFd(c);
continue;
} else if (errno == EINPROGRESS && !blocking) {
/* This is ok. */
} else if (errno == EADDRNOTAVAIL && reuseaddr) {
if (++reuses >= REDIS_CONNECT_RETRIES) {
goto error;
} else {
redisContextCloseFd(c);
goto addrretry;
}
} else {
if (redisContextWaitReady(c,timeout_msec) != REDIS_OK)
goto error;
}
}
if (blocking && redisSetBlocking(c,1) != REDIS_OK)
goto error;
if (redisSetTcpNoDelay(c) != REDIS_OK)
goto error;
c->flags |= REDIS_CONNECTED;
rv = REDIS_OK;
goto end;
}
if (p == NULL) {
char buf[128];
snprintf(buf,sizeof(buf),"Can't create socket: %s",strerror(errno));
__redisSetError(c,REDIS_ERR_OTHER,buf);
goto error;
}
error:
rv = REDIS_ERR;
end:
freeaddrinfo(servinfo);
return rv; // Need to return REDIS_OK if alright
}
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
本节完。