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;
}