IP地址结构信息与字符串相互转化:inet_pton和inet_ntop, etc.
1. 问题描述
1)假如有一个IPv4地址字符串(点分十进制),如何转化为socket编程所需的sockaddr地址结构信息?
2)假如通过getaddrinfo等方式,已经取得了sockaddr地址结构信息,如何转化为字符串形式?(点分十进制 for IPv4, 冒号十六进制 for IPv6)
什么是IP地址文本形式?
点分十进制数串: 形如"192.168.0.1"
冒号十六进制数串:形如"fe80::20c:29ff:fe99:de11"
2. IPv4场景解决方案
inet_aton, inet_addr, inet_ntoa在点分十进制数串与长度为32bits的网络字节序二进制值间转换IPv4地址。 不适用于IPv6地址。
2.1 inet_aton函数
点分十进制数串 -> IPv4地址结构(in_addr)
将主机地址cp(点分十进制数串)转换成一个32bits的网络字节序二进制值,保存到inp指向的缓存。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
-
参数
cp 点分十进制数串,由调用者提供, 通常cp采用形如a.b.c.d形式, 其他支持的形式参见man inet_aton手册
inp 输出参数, 指向in_addr结构。由调用者维护缓存, 函数填写内容 -
返回值
若cp指向的地址有效,返回非0;若无效,返回0
特别地,
如果inp为NULL,那么该函数仍然将输入的字符串执行有效性检查,但是不存储任何结果
2.2 inet_addr函数
点分十进制数串 -> IPv4地址结构(in_addr)
inet_addr同inet_aton的区别是, inet_addr返回32bits的网络字节序二进制值
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
该函数有个特殊情况,
所有232个可能值都是有效IP地址(0.0.0.0 到 255.255.255.255),但是出错时,函数返回INADDR_NONE(通常-1,对应32个全1的值),也就是说点分十进制数串255.255.255.255(IPv4有限广播地址)不能由该函数处理。
因此,应当避免使用inet_addr
2.3 inet_ntoa函数
IPv4地址结构(in_addr) -> 点分十进制数串
函数将32bits主机地址in转化成点分十进制数串
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
-
参数
in 调用者传入,IPv4地址结构对象。注意到与inet_aton的参数inp的区别,in并非指针 -
返回值
返回static分配的缓存, 后续的调用会覆写之前的内容。因此,如果不想被覆盖,需要自行分配存储进行复制、保存。
2.4 in_addr和sockaddr_in地址结构
可以从下面看到,in_addr是sockaddr_in的一个成员,而in_addr实际上是一个32bit的无符号整型,不过被包装成的结构体。
这也就是说, 如果是想直接将点分十进制数串和ip地址信息相互转换时, 应该是与sockaddr_in.sin_addr进行转化,而不能直接与sockaddr_in对象或s_addr进行转换。
sockaddr_in结构:
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
in_addr结构:
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr;
};
2.4 示例
通过inet_aton,inet_addr,将指定点分十进制ip文本串转化为二进制数;然后,通过inet_ntoa将指定ip地址二进制数转化为文本串,观察是否能还原文本串。
int test()
{
char *ipstr = "192.168.0.1";
// char *ipstr = "255.255.255.255";
struct sockaddr_in sockaddr;
int ret;
char *str;
/* convert ip from text to binary for IPv4 */
ret = inet_aton(ipstr, &sockaddr.sin_addr);
if (ret == 0) {
fprintf(stderr, "inet_aton error: invalid ip string: %s\n", ipstr);
return -1;
}
printf("inet_aton convert ip : from %s to %d\n", ipstr, sockaddr.sin_addr.s_addr);
/* convert ip from text to binary for IPv4 except 255.255.255.255 */
sockaddr.sin_addr.s_addr = inet_addr(ipstr); /* avoid to use inet_addr */
if (sockaddr.sin_addr.s_addr == INADDR_NONE) {
fprintf(stderr, "inet_addr error: invalid ip string: %s\n", ipstr);
return -1;
}
printf("inet_addr convert ip : from %s to %d\n", ipstr, sockaddr.sin_addr.s_addr);
/* convert ip from binary to text for IPv4 */
str = inet_ntoa(sockaddr.sin_addr);
if (str == NULL) {
fprintf(stderr, "inet_ntoa error: invalid ip structure: %d\n", sockaddr.sin_addr.s_addr);
return -1;
}
printf("inet_ntoa convert ip: from %d to %s\n", sockaddr.sin_addr.s_addr, str);
return 0;
}
运行结果:
inet_aton convert ip : from 192.168.0.1 to 16820416
inet_addr convert ip : from 192.168.0.1 to 16820416
inet_ntoa convert ip: from 16820416 to 192.168.0.1
3. IPv4,IPv6场景共同解决方案
3.1 inet_pton函数
IPv4和IPv6地址: 文本 -> 二进制
将地址文本串 转换成 网络字节序二进制数形式
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
- 参数
af 地址家族,支持的取值(且只能是这2个):
1)AF_INET 适用于IPv4地址,只支持"ddd.ddd.ddd.ddd"这种点分十进制数串
2)AF_INET6 适用于IPv6地址
注意:这里不同于socket()的af(地址family/domain),没有AF_UNIX
src 指向IP地址的文本形式的字符串(点分十进制数串 for IPv4,冒号十六进制数串 for IPv6)
dst 指向网络地址结构(struct in_addr for IPv4, struct struct in_addr6 for IPv6)
- 返回值
成功,返回1(网络地址成功转换);如果在指定地址家族中,src不是一个有效网络地址文本串,则返回0;如果af不包含一个有效地址家族(非规定2个取值),则返回-1, 且errno设置为EAFNOSUPPORT。
3.2 inet_ntop函数
IPv4和IPv6地址: 二进制 -> 文本
将地址网络字节序二进制数 转换成 文本串形式
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 参数
af 地址家族,支持的取值(且只能是这2个):
1)AF_INET 适用于IPv4地址,只支持"ddd.ddd.ddd.ddd"这种点分十进制数串
2)AF_INET6 适用于IPv6地址
注意:这里不同于socket()的af(地址family/domain),没有AF_UNIX
src 指向二进制地址结构,对于ipv4, src指向struct in_addr;对于ipv6,src指向struct in_addr6
dst 指向存放地址文本串的缓存,由调用者维护缓存, 函数填充内容
size 指示dst指向缓存大小
- 返回值
成功,返回指向dst的非NULL指针;失败, 返回NULL,errno被设置
3.3 示例
对于ipv4,方式同inet_aton, inet_addr, inet_ntoa,先将文本串转化为二进制数,打印;然后再转化为文本串,再打印。观察中间输出,以及最终是否能还原。
对于ipv6,因为ipv6地址二进制数128bit, 这里没有直接打印二进制数,而是先将已知的ipv6文本串(冒号十六进制表示法)先通过inet_pton转化为二进制串,再通过inet_ntop转换回文本串。观察是否能还原。
int test()
{
/* ipv4 solution */
struct sockaddr_in sockaddr;
char *ipstr = "192.168.0.1";
char ip4text[INET_ADDRSTRLEN];
char *str;
int ret;
printf("===ipv4===\n");
/* convert ip from text to binary for IPv4 */
bzero(&sockaddr, sizeof (sockaddr));
ret = inet_pton(AF_INET, ipstr, &sockaddr.sin_addr);
if (ret != 1) {
if (ret == 0) {
fprintf(stderr, "inet_pton error: %s is not a valid ip text\n", ipstr);
}
else if (ret == -1) {
perror("inet_pton error");
}
fprintf(stderr, "inet_pton error: unkown error");
return -1;
}
printf("inet_pton convert ip from %s to %d\n", ipstr, sockaddr.sin_addr.s_addr);
/* convert ip from binary to text for IPv4 */
str = inet_ntop(AF_INET, &sockaddr.sin_addr, ip4text, sizeof(ip4text));
if (str == NULL) {
perror("inet_ntop error");
return -1;
}
printf("inet_ntop convert ip from %d to %s\n", sockaddr.sin_addr.s_addr, str);
/* ipv6 solution */
char *ip6str = "fe80::20c:29ff:fe99:de11"; /* inet6 from bash "$ ifconfig -a" */
char ip6text[INET6_ADDRSTRLEN];
struct sockaddr_in6 sockaddr6;
printf("===ipv6===\n");
/* convert ip from text to binary for IPv6 */
bzero(&sockaddr6, sizeof (sockaddr6));
ret = inet_pton(AF_INET6, ip6str, &sockaddr6.sin6_addr);
if (ret != 1) {
if (ret == 0) {
fprintf(stderr, "inet_pton error: %s is not a valid ipv6 text\n", ip6str);
}
else if (ret == -1) {
perror("inet_pton error");
}
fprintf(stderr, "inet_pton error: unkown error");
return -1;
}
/* convert ip from binary to text for IPv6 */
str = inet_ntop(AF_INET6, &sockaddr6.sin6_addr, ip6text, sizeof(ip6text));
if (str == NULL) {
perror("inet_ntop error");
return -1;
}
printf("%s\n", str);
}
运行结果:
===ipv4===
inet_pton convert ip from 192.168.0.1 to 16820416
inet_ntop convert ip from 16820416 to 192.168.0.1
===ipv6===
fe80::20c:29ff:fe99:de11
3.4 sock_ntop自定义函数
inet_ntop存在一个问题:要求调用者传递一个指向某个二进制地址的指针src,而该指针通常包含在一个套接字地址结构中,也就是说,调用者必须直到这个结构的格式,地址家族,以及应该具体为哪个IP格式提供各自的地址结构(struct sockaddr_in for IPv4, struct sockaddr_in6 for IPv6)
i.e. 对IPv4,应该编写这样的代码:
struct sockaddr_in sockaddr;
inet_ntop(AF_INET, &sockaddr.sin_addr, str, sizeof(str));
对IPv6,应该编写这样的代码:
struct sockaddr_in6 sockaddr6;
inet_ntop(AF_INET6, &sockaddr6.sin_addr, str, sizeof(str));
这样,就使得代码的编写与协议相关。为解决该问题,编写sock_ntop,以指向某个套接字地址结构的指针为参数,查看该结构的内部,然后根据具体情况返回地址的文本串格式
char *sock_ntop(const struct sockaddr *sa, socklen_t addrlen);
函数实现
char *sock_ntop(const struct sockaddr *sa, socklen_t addrlen)
{
char portstr[8];
static char str[128]; /* struct sockaddr_storage最多128byte */
switch (sa->sa_family) {
case AF_INET: {
struct sockaddr_in *sin = (struct sockaddr_in *)sa;
if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
return NULL; /* 转换存在错误 */
if (ntohs(sin->sin_port) != 0) {
snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
strcat(str, portstr);
}
return str;
}
case AF_INET6: {
struct sockaddr_in *sin6 = (struct sockaddr_in *)sa;
if (inet_ntop(AF_INET6, &sin6->sin_addr, str, sizeof(str)) == NULL)
return NULL; /* 转换存在错误 */
if (ntohs(sin6->sin_port) != 0) {
snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin6->sin_port));
strcat(str, portstr);
}
return str;
}
default:
errno = EAFNOSUPPORT; /* 不支持的地址family */
}
return NULL;
}