C语言如何获取ipv6地址
使用通常獲取ipv4的IP地址的方法是無(wú)法獲取ipv6地址的,本文介紹了使用C語(yǔ)言獲取ipv6地址的三種方法:從proc文件從系統(tǒng)獲取ipv6地址、使用getifaddrs()函數(shù)獲取ipv6地址和使用netlink獲取ipv6地址,每種方法均給出了完整的源程序,本文所有實(shí)例在 ubuntu 20.04 下測(cè)試通過(guò),gcc 版本 9.4.0。
1. ipv4的IP地址的獲取方法
-
不論是獲取 ipv4 的 IP 地址還是 ipv6 的地址,應(yīng)用程序都需要與內(nèi)核通訊才可以完成;
-
ioctl 是和內(nèi)核通訊的一種常用方法,也是用來(lái)獲取 ipv4 的 IP 地址的常用方法,下面代碼演示了如何使用 ioctl 來(lái)獲取本機(jī)所有接口的 IP 地址:
#include <stdio.h> #include <stdlib.h>#include <sys/ioctl.h> #include <linux/if.h> #include <arpa/inet.h> #include <sys/socket.h>int main() {int i = 0;int sockfd;struct ifconf ifc;char buf[512] = {0};struct ifreq *ifr;ifc.ifc_len = 512;ifc.ifc_buf = buf;if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket");return -1;}ioctl(sockfd, SIOCGIFCONF, &ifc);ifr = (struct ifreq*)buf;for (i = (ifc.ifc_len /sizeof(struct ifreq)); i > 0; i--) {printf("%s: %s\n",ifr->ifr_name, inet_ntoa(((struct sockaddr_in *)&(ifr->ifr_addr))->sin_addr));ifr++;} } -
但是使用 ioctl 無(wú)法獲取 ipv6 地址,即便我們建立一個(gè) AF_INET6 的 socket,ioctl 仍然只返回 ipv4 的信息,我們可以試試下面代碼;
#include <stdio.h> #include <stdlib.h>#include <sys/ioctl.h> #include <linux/if.h> #include <arpa/inet.h> #include <sys/socket.h>int main() {int i = 0;int sockfd;struct ifconf ifc;char buf[1024] = {0};struct ifreq *ifr;ifc.ifc_len = 1024;ifc.ifc_buf = buf;if ((sockfd = socket(AF_INET6, SOCK_DGRAM, 0)) < 0) {perror("socket");return -1;}ioctl(sockfd, SIOCGIFCONF, &ifc);ifr = (struct ifreq*)buf;struct sockaddr_in *sa;for (i = (ifc.ifc_len /sizeof(struct ifreq)); i > 0; i--) {sa = (struct sockaddr_in *)&(ifr->ifr_addr);if (sa->sin_family == AF_INET6) {printf("%s: AF_INET6\n", ifr->ifr_name);} else if (sa->sin_family == AF_INET){printf("%s: AF_INET\n", ifr->ifr_name);} else {printf("%s: %d. It is an unknown address family.\n", ifr->ifr_name, sa->sin_family);}ifr++;} } -
這段程序在我的機(jī)器上的運(yùn)行結(jié)果是這樣的:
圖1:ioctl無(wú)法獲取ipv6地址
- 我們看到,不管怎么折騰,返回的仍然只有 ipv4 的地址,所以我們需要一些其他的方法獲得 ipv6 地址,下面介紹三種使用 C 語(yǔ)言獲得 ipv6 地址的方法。
2. 從文件/proc/net/if_inet6中獲取ipv6地址
-
我們先來(lái)看看文件/proc/net/if_inet6中有什么內(nèi)容
圖2:文件/proc/net/if_inet6內(nèi)容
-
這個(gè)文件中,每行為一個(gè)網(wǎng)絡(luò)接口的數(shù)據(jù),每行數(shù)據(jù)分成 6 個(gè)字段
字段序號(hào)字段名稱字段說(shuō)明 1 ipv6address IPv6地址,32位16進(jìn)制一組,中間沒(méi)有:分隔符 2 ifindex 接口設(shè)備號(hào),每個(gè)設(shè)備都不同,按 16 進(jìn)制顯示 3 prefixlen 16進(jìn)制顯示的前綴長(zhǎng)度,類似 ipv4 的子網(wǎng)掩碼的東西 4 scopeid scope id 5 flags 接口標(biāo)志,這些標(biāo)志標(biāo)識(shí)著這個(gè)接口的特性 6 devname 接口設(shè)備名稱 -
所以從這個(gè)文件中可以很容易地獲得所有接口的 ipv6 地址
#include <stdio.h> #include <linux/if.h> #include <netinet/in.h> #include <arpa/inet.h>int main(void) {FILE *f;int scope, prefix;unsigned char _ipv6[16];char dname[IFNAMSIZ];char address[INET6_ADDRSTRLEN];f = fopen("/proc/net/if_inet6", "r");if (f == NULL) {return -1;}while (19 == fscanf(f," %2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx\%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx \%*x %x %x %*x %s",&_ipv6[0], &_ipv6[1], &_ipv6[2], &_ipv6[3], &_ipv6[4], &_ipv6[5], &_ipv6[6], &_ipv6[7],&_ipv6[8], &_ipv6[9], &_ipv6[10], &_ipv6[11], &_ipv6[12], &_ipv6[13], &_ipv6[14], &_ipv6[15],&prefix, &scope, dname)) {if (inet_ntop(AF_INET6, _ipv6, address, sizeof(address)) == NULL) {continue;}printf("%s: %s\n", dname, address);}fclose(f);return 0; } -
fscanf 中的 %2hhx 是一種不多見(jiàn)的用法,hhx 表示后面的指針 &_ipv6[x] 指向一個(gè) unsigned char *,2 表示從文件中讀取的長(zhǎng)度,這個(gè)是常用的;
-
關(guān)于 fscanf 中的 hh 和 h 的用法,可以查看在線手冊(cè) man fscanf 了解更多的內(nèi)容;
-
ipv6 地址一共 128 位,16 位一組,一共 8 組,但是這里為什么不一次從文件中讀入 4 個(gè)字符(16 位),讀 8 次,而要一次讀入 2 個(gè)字符讀 16 次呢?
- 這個(gè)要去看 inet_ntop 的參數(shù),我們先使用命令 man inet_ntop 看一下 inet_ntop 的在線手冊(cè)const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 當(dāng)?shù)?1 個(gè)參數(shù) af = AF_INET6 時(shí),對(duì)于第 2 個(gè)參數(shù),還有說(shuō)明:AF_INET6src points to a struct in6_addr (in network byte order) which is converted to a representation ofthis address in the most appropriate IPv6 network address format for this address. The buffer dstmust be at least INET6_ADDRSTRLEN bytes long.
- 很顯然,需要第 2 個(gè)參數(shù)指向一個(gè) struct in6_addr,這個(gè)結(jié)構(gòu)在 netinet/in.h 中定義:/* IPv6 address */ struct in6_addr {union{uint8_t __u6_addr8[16];uint16_t __u6_addr16[8];uint32_t __u6_addr32[4];} __in6_u; #define s6_addr __in6_u.__u6_addr8 #ifdef __USE_MISC # define s6_addr16 __in6_u.__u6_addr16 # define s6_addr32 __in6_u.__u6_addr32 #endif };
- 這個(gè)結(jié)構(gòu)在一般情況下使用的是 uint8_t __u6_addr8[16],也就是 16 個(gè) unsigned char 的數(shù)組;
- 所以,實(shí)際上 struct in6_addr 的結(jié)構(gòu)如下struct in6_addr {unsigned char __u6_addr8[16]; }
- 這就是我們?cè)谧x文件時(shí)為什么要一次讀入 2 個(gè)字符,讀 16 次,要保證讀出的內(nèi)容符合 struct in6_addr 的定義;
-
一次從文件中讀入 4 個(gè)字符(16 位),讀 8 次,和一次讀入 2 個(gè)字符讀 16 次有什么不同呢?我們以 16 進(jìn)制的 f8e9 為例
- 當(dāng)我們每次讀入 2 個(gè)字符,讀 2 次時(shí),在內(nèi)存中的排列是這樣的unsigned char _ipv6[16]; fscanf(f, "%2hhx2hhx", &_ipv6[0], &_ipv6[1]); unsigned char *p = _ipv6 f8 e9 -+ -+| || +------- p + 1+----------- p
- 當(dāng)我們每次讀入 4 個(gè)字符,讀 1 次時(shí),在內(nèi)存中的排列是這樣的unsigned int _ipv6[8] fscanf(f, "%4x", &_ipv6[0]) unsigned char *p = (unsigned char *)_ipv6 e9 f8 -+ -+| || +------- p + 1+----------- p
- 這是因?yàn)?X86 系列 CPU 的存儲(chǔ)模式是小端模式,也就是高位字節(jié)要存放在高地址上,f8e9 這個(gè)數(shù),f8 是高位字節(jié),e9 是低位字節(jié),所以當(dāng)我們把 f8e9 作為一個(gè)整數(shù)讀出的時(shí)候,e9 將存儲(chǔ)在低地址,f8 存儲(chǔ)在高地址,這和 struct in6_addr 的定義是不相符的;
- 所以如果我們一次讀 4 個(gè)字符, 讀 8 次,我們就不能使用 inet_ntop() 去把 ipv6 地址轉(zhuǎn)換成我們所需要的字符串,當(dāng)然我們可以自己轉(zhuǎn)換,但有些麻煩,參考下面代碼unsigned short int _ipv6[8]; int zero_flag = 0; while (11 == fscanf(f," %4hx%4hx%4hx%4hx%4hx%4hx%4hx%4hx %*x %x %x %*x %s",&_ipv6[0], &_ipv6[1], &_ipv6[2], &_ipv6[3], &_ipv6[4], &_ipv6[5], &_ipv6[6], &_ipv6[7],&prefix, &scope, dname)) {printf("%s: ", dname);for (int i = 0; i < 8; ++i) {if (_ipv6[i] != 0) {if (i) putc(':', stdout); printf("%x", _ipv6[i]);zero_flag = 0;} else {if (!zero_flag) putc(':', stdout);zero_flag = 1;}}putc('\n', stdout); }
- 和上面的代碼比較,多了不少麻煩,自己去體會(huì)吧。
3. 使用getifaddrs()獲取 ipv6 地址
-
可以通過(guò)在線手冊(cè) man getifaddrs 了解詳細(xì)的關(guān)于 getifaddrs 函數(shù)的信息;
-
getifaddrs 函數(shù)會(huì)創(chuàng)建一個(gè)本地網(wǎng)絡(luò)接口的結(jié)構(gòu)鏈表,該結(jié)構(gòu)鏈表定義在 struct ifaddrs 中;
-
關(guān)于 ifaddrs 結(jié)構(gòu)有很多文章介紹,本文僅簡(jiǎn)單介紹一下與本文密切相關(guān)的內(nèi)容,下面是 struct ifaddrs 的定義
struct ifaddrs {struct ifaddrs *ifa_next; /* Next item in list */char *ifa_name; /* Name of interface */unsigned int ifa_flags; /* Flags from SIOCGIFFLAGS */struct sockaddr *ifa_addr; /* Address of interface */struct sockaddr *ifa_netmask; /* Netmask of interface */union {struct sockaddr *ifu_broadaddr;/* Broadcast address of interface */struct sockaddr *ifu_dstaddr;/* Point-to-point destination address */} ifa_ifu; #define ifa_broadaddr ifa_ifu.ifu_broadaddr #define ifa_dstaddr ifa_ifu.ifu_dstaddrvoid *ifa_data; /* Address-specific data */ }; -
ifa_next 是結(jié)構(gòu)鏈表的后向指針,指向鏈表的下一項(xiàng),當(dāng)前項(xiàng)為最后一項(xiàng)時(shí),該指針為 NULL;
-
ifa_addr 是本文主要用到的項(xiàng),這是一個(gè) struct sockaddr, 看一下 struct sockaddr 的定義:
struct sockaddr {sa_family_t sa_family;char sa_data[14]; } -
實(shí)際上,當(dāng) ifa_addr->sa_family 為 AF_INET 時(shí),ifa_addr 指向 struct sockaddr_in;當(dāng) ifa_addr->sa_family 為 AF_INET6 時(shí),ifa_addr 指向一個(gè) struct sockaddr_in6;
-
sockaddr_in 和 sockaddr_in6 這兩個(gè)結(jié)構(gòu)同樣可以找到很多介紹文章,這里就不多說(shuō)了,反正這里面是結(jié)構(gòu)套著結(jié)構(gòu),要把思路捋順了才不至于搞亂;
-
下面是使用 getifaddrs() 獲取 ipv6 地址的源程序,可以看到,打印 ipv6 地址的那幾行,與上面的那個(gè)例子是一樣的;
#include <arpa/inet.h> #include <ifaddrs.h> #include <stdio.h> #include <stdlib.h>int main () {struct ifaddrs *ifap, *ifa;struct sockaddr_in6 *sa;char addr[INET6_ADDRSTRLEN];if (getifaddrs(&ifap) == -1) {perror("getifaddrs");exit(1);}for (ifa = ifap; ifa; ifa = ifa->ifa_next) {if (ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET6) {// 打印ipv6地址sa = (struct sockaddr_in6 *)ifa->ifa_addr;if (inet_ntop(AF_INET6, (void *)&sa->sin6_addr, addr, INET6_ADDRSTRLEN) == NULL)continue;printf("%s: %s\n", ifa->ifa_name, addr);}}freeifaddrs(ifap);return 0; } -
最后要注意的是,使用 getifaddrs() 后,一定要記得使用 freeifaddrs() 釋放掉鏈表所占用的內(nèi)存。
-
這個(gè)例子中,我們使用 inet_ntop() 將 sin6_addr 結(jié)構(gòu)轉(zhuǎn)換成了字符串形式的 ipv6 地址,還可以使用 getnameinfo() 來(lái)獲取 ipv6 的字符串形式的地址;
-
可以通過(guò)在線手冊(cè) man getnameinfo 了解 getnameinfo() 的詳細(xì)信息
-
下面是使用 getifaddrs() 獲取 ipv6 地址并使用 getnameinfo() 將將 ipv6 地址轉(zhuǎn)變?yōu)樽址脑闯绦?/p> #include <arpa/inet.h> #include <ifaddrs.h> #include <stdio.h> #include <stdlib.h>#include <netdb.h>int main () {struct ifaddrs *ifap, *ifa;char addr[INET6_ADDRSTRLEN];if (getifaddrs(&ifap) == -1) {perror("getifaddrs");exit(1);}for (ifa = ifap; ifa; ifa = ifa->ifa_next) {if (ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET6) {// 打印ipv6地址if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in6), addr, sizeof(addr), NULL, 0, NI_NUMERICHOST))continue;printf("%s: %s\n", ifa->ifa_name, addr);}}freeifaddrs(ifap);return 0; }
-
和前面那個(gè)程序相比,這個(gè)程序增加了一個(gè)包含文件 netdb.h,這里面有 getnameinfo() 的一些相關(guān)定義;
-
在這里使用函數(shù) getnameinfo 時(shí),要明確 ifa->ifa_addr 指向的是一個(gè) struct sockaddr_in6,后面的常數(shù) NI_NUMERICHOST 表示返回的主機(jī)地址為數(shù)字字符串;
-
和上面的例子略有不同的是,使用 getnameinfo 獲取的 ipv6 地址的最后會(huì)使用 ‘%’ 連接一個(gè)網(wǎng)絡(luò)接口的名稱,如下圖所示:
圖3:使用getnameinfo獲取ipv6地址
4. 使用 netlink 獲取 ipv6 地址
- netlink socket 是用戶空間與內(nèi)核空間通信的又一種方法,本文并不討論 netlink 的編程方法,但給出了使用 netlink 獲取 ipv6 地址的源程序;
- 與上面兩個(gè)方法比較,使用 netlink 獲取 ipv6 地址的方法略顯復(fù)雜,在實(shí)際應(yīng)用中并不多見(jiàn),所以本文也就不進(jìn)行更多的討論了;
- 下面是使用 netlink 獲取 ipv6 地址的源程序#include <asm/types.h> #include <arpa/inet.h> #include <linux/netlink.h> #include <linux/rtnetlink.h> #include <sys/socket.h> #include <string.h> #include <stdio.h>int main(int argc, char ** argv) {char buf1[16384], buf2[16384];struct {struct nlmsghdr nlhdr;struct ifaddrmsg addrmsg;} msg1;struct {struct nlmsghdr nlhdr;struct ifinfomsg infomsg;} msg2;struct nlmsghdr *retmsg1;struct nlmsghdr *retmsg2;int len1, len2;struct rtattr *retrta1, *retrta2;int attlen1, attlen2;char pradd[128], prname[128];int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);memset(&msg1, 0, sizeof(msg1));msg1.nlhdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg));msg1.nlhdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ROOT;msg1.nlhdr.nlmsg_type = RTM_GETADDR;msg1.addrmsg.ifa_family = AF_INET6;memset(&msg2, 0, sizeof(msg2));msg2.nlhdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));msg2.nlhdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ROOT;msg2.nlhdr.nlmsg_type = RTM_GETLINK;msg2.infomsg.ifi_family = AF_UNSPEC;send(sock, &msg1, msg1.nlhdr.nlmsg_len, 0);len1 = recv(sock, buf1, sizeof(buf1), 0);retmsg1 = (struct nlmsghdr *)buf1;while NLMSG_OK(retmsg1, len1) {struct ifaddrmsg *retaddr;retaddr = (struct ifaddrmsg *)NLMSG_DATA(retmsg1);int iface_idx = retaddr->ifa_index;retrta1 = (struct rtattr *)IFA_RTA(retaddr);attlen1 = IFA_PAYLOAD(retmsg1);while RTA_OK(retrta1, attlen1) {if (retrta1->rta_type == IFA_ADDRESS) {inet_ntop(AF_INET6, RTA_DATA(retrta1), pradd, sizeof(pradd));len2 = recv(sock, buf2, sizeof(buf2), 0);send(sock, &msg2, msg2.nlhdr.nlmsg_len, 0);len2 = recv(sock, buf2, sizeof(buf2), 0);retmsg2 = (struct nlmsghdr *)buf2;while NLMSG_OK(retmsg2, len2) {struct ifinfomsg *retinfo;retinfo = NLMSG_DATA(retmsg2);memset(prname, 0, sizeof(prname));if (retinfo->ifi_index == iface_idx) {retrta2 = IFLA_RTA(retinfo);attlen2 = IFLA_PAYLOAD(retmsg2);while RTA_OK(retrta2, attlen2) {if (retrta2->rta_type == IFLA_IFNAME) {strcpy(prname, RTA_DATA(retrta2));break;}retrta2 = RTA_NEXT(retrta2, attlen2);}break;}retmsg2 = NLMSG_NEXT(retmsg2, len2); }printf("%s: %s\n", prname, pradd);}retrta1 = RTA_NEXT(retrta1, attlen1);}retmsg1 = NLMSG_NEXT(retmsg1, len1); }return 0; }
5. 結(jié)語(yǔ)
- 本文給出了三種獲取 ipv6 地址的方法,均給出了完整的源程序;
- 本文對(duì)三種方法并沒(méi)有展開(kāi)討論,以免文章冗長(zhǎng);
- 僅就獲取 ipv6 地址而言,前兩種方法比較常用而且簡(jiǎn)單;
- 通常認(rèn)為,用戶程序與內(nèi)核通訊有四種方法
- 系統(tǒng)調(diào)用
- 虛擬文件系統(tǒng)(/proc、/sys等)
- ioctl
- netlink
- 本文所述的三個(gè)方法,正是使用了上述 2、3、4 三種方法;而獲取 ipv6 地址,簡(jiǎn)單地使用系統(tǒng)調(diào)用無(wú)法實(shí)現(xiàn)。
email: hengch@163.com
總結(jié)
以上是生活随笔為你收集整理的C语言如何获取ipv6地址的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 金蝶云星空初级实施
- 下一篇: linux的vps主机安装图形界面并远程