协议无关方法
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;
}