协议无关方法
Linux 提供了很多强大的函数,实现了二进制套接字地址结构和主机名、主机地址、服务名和端口号的字符串表示之间的相互转化。若和套接字一起使用,就可以开发出独立于协议的网络程序。
0、数据结构
在一切开始之前,我们需要简单回顾表示二进制套接字地址结构的数据结构 addrinfo。因为接下来介绍的函数都与它密切相关。
struct addrinfo {
int ai_flags; /* Input flags. */
int ai_family; /* Protocol family for socket. */
int ai_socktype; /* Socket type. */
int ai_protocol; /* Protocol for socket. */
char *ai_cannonname; /* Canonical name for service location. */
size_t ai_addrlen; /* Length of socket address. */
struct sockaddr *ai_addr; /* Socket address for socket. */
struct addrinfo *ai_next; /* Length of socket address. */
}
addrinfo 中的变量可以直接传递到套接字接口中。其中
ai_family表示选定的协议族ai_socktype表示套接字类型ai_protocol表示套接字协议类型,以上三类可直接传递给socket()函数做参数 。ai_addr指向套接字结构体ai_addrlen表示套接字地址的字节长度,以上两个可直接做参数传递给connect()和bind()。ai_next指向列表中下一个addrinfo结构,该字段方便函数遍历查找。ai_flags是一个位掩码,通过如下标志定义:
| 标志 | 描述 |
|---|---|
| AI_ADDRCONFIG | 查询配置的地址类型(IPv4 或 IPv6)。如果使用连接,推荐使用该标志 |
| AI_ALL | 查找 IPv4 和 IPv6 地址(仅用于AI_V4MAPPED) |
| AI_CANONNAME | 需要一个规范的名字,默认为 NULL |
| AI_NUMERICHOST | 以数字地址方式指定主机地址 |
| AI_NUMERICSERV | 以数字地址方式指定数字端口号 |
| AI_PASSIVE | 套接字地址用于绑定监听 |
| AI_V4MPPED | 若未找到 IPv6 的地址,返回映射到IPv6 格式的 IPv4 地址 |
将这些二进制套接字地址结构封装完成后,编写客户端、服务端程序就无需关注于某一个 IP 协议,只要流程一致即可。
接下来介绍一下在编写协议无关程序时常用的几个 API 函数。
1、getaddrinfo 函数
简介
getaddrinfo 函数将主机名、主机地址、服务名和端口号的字符串转化为二进制套接字结构。作为 gethostbyname 和 getservbyname 函数的替代品,它是可重入的,适用于任何协议。
#include <sys/types.h>
#include <sys/socket.h>
int getaddrinfo(const char *host, /* host 或者IP地址 */
const char *service, /* 十进制端口号 或者常用服务名称如"ftp"、"http"等 */
const struct addrinfo *hints, /* 获取信息要求设置 */
struct addrinfo **res); /* 获取信息结果 */
getaddrinfo() 函数一共有四个参数:
host表示套接字地址中的 IP 地址,可以是域名,也可以是数字地址,如 IPv4 的点分十进制串。但若hints参数中的ai_flags中设置了AI_NUMERICHOST标志,那么该参数只能是数字地址,不能是域名。service表示端口号,但也可以用来表示服务名称,如 http 等。如果不想把主机名转换成地址,可以把host设置为NULL,对service来说也是一样。但是host和service同时只能有一个为NULL。hints参数指向前文提到的addrinfo结构体,是为方便用户更好地控制返回的套接字列表。即对返回的套接字列表提出“要求”,因而用户只能设定其中ai_family、ai_socktype、ai_protocol和ai_flags四个域,其他域必须设置为0 或者NULL,因此通常是先使用memset()初始化后再设定指定的四个域。result参数是getaddrinfo的返回值,它是一个指向addrinfo结构的链表的指针。其中每个结构指向一个对应于host和service的套接字地址结构,如下图。

客户端调用后,会遍历该列表,依次尝试每个套接字地址,直到调用 socket 和 connect 成功。而服务器调用时,也会依次尝试套接字地址,直到 socket 和 bind 成功。
此外,freeaddrinfo() 用以释放该链表。若出错,调用 gai_strerror 函数可将错误代码转为字符串。
#include <sys/types.h>
#include <sys/socket.h>
void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode);
示例用法
使用 CSAPP 中例子能很好地说明该函数的用法。CSAPP 中实现了 open_listenfd 的辅助函数,用于打开和返回一个监听描述符,这个描述符准备好在端口 port 上接受连接请求。该函数是可重入的,可协议独立的。
int open_listenfd(char *port) {
struct addrinfo hints, *listp, *p;
int listenfd, rc, optval = 1;
/* 通过 getaddrinfo 函数获得可能的服务地址 */
/* 与前文说到那样,使用 hints 参数前先全部置为 0 */
memset(&hints, 0, sizeof(struct addrinfo));
/* Internet连接 监听端口 IPv4 使用数字地址端口 */
hints.ai_socktype = SOCK_STREAM; /* Accept connections */
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
// 若执行成功 则返回 0
if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port,
gai_strerror(rc));
return -2;
}
/* 遍历列表,找到可以bind的套接字接口 */
for (p = listp; p; p = p->ai_next) {
/* 使用 addrinfo 中的变量创建套接字 */
/* 如果失败就遍历下一个 */
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue;
/* Eliminates "Address already in use" error from bind */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
(const void *)&optval, sizeof(int));
/* 绑定对应的套接字 */
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break;
/* 若失败就下一个 */
if (close(listenfd) < 0) {
fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
return -1;
}
}
/* 最后使用 freeaddrinfo函数清除列表 */
freeaddrinfo(listp);
if (!p)
return -1;
/* 监听端口开始工作,并返回 */
if (listen(listenfd, LISTENQ) < 0) {
close(listenfd);
return -1;
}
return listenfd;
}
2、getnameinfo 函数
简介
getnameinfo 函数与 getaddrinfo 函数相反,它将一个套接字地址结构转换为相应的主机和服务名字符串。它是已弃用的 gethostbyaddr 和 getservbyport 的替代函数,是可重入,也是协议无关的。
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *service, size_t servlen, int flags);
参数 sa 指向大小为 salen 的套接字地址结构,host 指向大小为 hostlen 的对应主机的字符串,service 指向大小为 servlen 的对应服务名字符串。而参数 flags 是一个位掩码,能够修改默认行为。同样地,错误代码处理由 gai_strerror 负责。
对于参数 host ,如果不想要主机名,可以设置为 NULL,hostlen 设置为 0,同理可用在服务名,但两者不能全为 NULL 。
对于参数 flags,它是一个位掩码,其中下面两个值会经常使用到:
NI_NUMERICHOST函数默认视图返回一个数字地址的 host 域名NI_NUMERICSERV函数会简单地返回服务的端口号
示例程序
int main(int argc, char **argv) {
struct addrinfo hints, *listp, *p;
int flags, rc;
char buf[MAXLINE];
if (argc != 2) {
fprintf(stderr, "usage: %s ", argv[0]);
exit(0);
}
/* 通过 getaddrinfo 函数获得可能的服务地址 */
/* 与前文说到那样,使用 hints 参数前先全部置为 0 */
memset(&hints, 0, sizeof(struct addrinfo));
/* Internet连接 监听端口 IPv4 使用数字地址端口 */
hints.ai_family = AF_INET; /* Use IPv4 only */
hints.ai_socktype = SOCK_STREAM; /* Accept connections */
// 若执行成功 则返回 0
if ((rc = getaddrinfo(argv[1], port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port,
gai_strerror(rc));
exit(1);
}
/* 遍历列表套接字接口 */
for (p = listp; p; p = p->ai_next) {
/* 使用 getnameinfo 函数来将套接字地址转换为点分十进制串 */
getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
printf("%s\n", buf);
}
/* 最后使用 freeaddrinfo 函数清除列表 */
freeaddrinfo(listp);
return 0;
}
本文详细介绍了Linux中实现协议无关网络编程的关键函数——getaddrinfo和getnameinfo。getaddrinfo将主机名、服务名转化为套接字结构,便于客户端和服务器创建独立于特定协议的网络连接。getnameinfo则将套接字地址转换为主机和服务名字符串。这两个函数都是可重入且协议无关的,分别作为gethostbyname和getservbyname的替代,以及gethostbyaddr和getservbyport的替代。通过示例代码展示了如何使用这两个函数进行网络编程。

1083

被折叠的 条评论
为什么被折叠?



