C語言如何獲取ipv6地址
使用通常獲取ipv4的IP地址的方法是無法獲取ipv6地址的,本文介紹了使用C語言獲取ipv6地址的三種方法,每種方法均給出了完整的源程式,本文所有例項在ubuntu 20.04下測試通過,gcc版本9.4.0。
1. ipv4的IP地址的獲取方法
- 不論是獲取ipv4的IP地址還是ipv6的地址,應用程式都需要與核心通訊才可以完成;
- ioctl 是和核心通訊的一種常用方法,也是用來獲取ipv4的IP地址的常用方法,下面程式碼演示瞭如何使用ioctl來獲取本機所有介面的IP地址:
#include#include#include#include#include#includeint 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 ; } return 0; }
- 但是使用ioctl無法獲取ipv6地址,即便我們建立一個AF_INET6的socket,ioctl仍然只返回ipv4的資訊,我們可以試試下面程式碼;
#include#include#include#include#include#includeint 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 ; } }
- 這段程式在我的機器上的執行結果是這樣的:
圖1:ioctl無法獲取ipv6地址
- 我們看到,不管怎麼折騰,返回的仍然只有ipv4的地址,所以我們需要一些其他的方法獲得ipv6地址,下面介紹三種使用C語言獲得ipv6地址的方法。
2. 從檔案/proc/net/if_inet6中獲取ipv6地址
- 我們先來看看檔案/proc/net/if_inet6中有什麼內容:
圖2:檔案/proc/net/if_inet6內容
- 這個檔案中,每行為一個網路介面的資料,每行資料分成 6 個欄位
序號 | 欄位名稱 | 欄位說明 |
1 | ipv6address | ipv6地址,16位(4個字元)一組,16進位制,中間沒有分隔符 |
2 | ifindex | 介面裝置號,每個裝置不同 |
3 | prefixlen | 字首長度,類似子網掩碼 |
4 | scopeid | scope id |
5 | flags | 介面標誌,標識這個介面的特性 |
6 | devname | 介面裝置名稱 |
- 所以從這個檔案中可以很容易地獲得所有介面的 ipv6 地址:
#include#include#include#includeint 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是一種不多見的用法,hhx表示後面的指標&_ipv6[x]指向一個unsigned char *,2表示從檔案中讀取的長度,這個是常用的;
- 關於fscanf中的hh和h的用法,可以檢視線上手冊man fscanf瞭解更多的內容;
- ipv6地址一共128位,16位一組,一共8組,但是這裡為什麼不一次從檔案中讀入4個字元(16 位),讀8次,而要一次讀入2個字元讀16次呢?
這個要去看inet_ntop的引數,我們先使用命令man inet_ntop看一下inet_ntop的線上手冊:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
當第1個引數af=AF_INET6時,對於第2個引數,還有說明:
AF_INET6 src points to a struct in6_addr (in network byte order) which is converted to a representation of this address in the most appropriate IPv6 network address format for this address. The buffer dst must be at least INET6_ADDRSTRLEN bytes long.
很顯然,需要第2個引數指向一個struct in6_addr,這個結構在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 };
這個結構在一般情況下使用的是uint8_t __u6_addr8[16],也就是16個unsigned char的陣列,只有在"混雜模式"時才使用8個unsigned short int或者4個unsigned int的陣列;
所以,實際上struct in6_addr的結構如下:
struct in6_addr { unsigned char __u6_addr8[16]; }
這就是我們在讀檔案時為什麼要一次讀入2個字元,讀16次,要保證讀出的內容符合struct in6_addr的定義;
- 一次從檔案中讀入4個字元(16位),讀8次,和一次讀入2個字元讀16次有什麼不同呢?我們以16進位制的f8e9為例;
當我們每次讀入 2 個字元,讀 2 次時,在記憶體中的排列是這樣的:
unsigned char _ipv6[16]; fscanf(f, "%2hhx2hhx", &_ipv6[0], &_ipv6[1]); unsigned char *p = _ipv6 f8 e9 - - | | | ------- p 1 ----------- p
當我們每次讀入 4 個字元,讀 1 次時,在記憶體中的排列是這樣的:
unsigned int _ipv6[8] fscanf(f, "%4x", &_ipv6[0]) unsigned char *p = (unsigned char *)_ipv6 e9 f8 - - | | | ------- p 1 ----------- p
這是因為X86系列CPU的儲存模式是小端模式,也就是高位位元組要存放在高地址上,f8e9這個數,f8是高位位元組,e9是低位位元組,所以當我們把f8e9作為一個整數讀出的時候,e9 將儲存在低地址,f8儲存在高地址,這和struct in6_addr的定義是不相符的;
所以如果我們一次讀4個字元,讀8次,我們就不能使用inet_ntop()去把ipv6地址轉換成我們所需要的字串,當然我們可以自己轉換,但有些麻煩,參考下面程式碼:
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); }
和上面的程式碼比較,多了不少麻煩,自己去體會吧。
3. 使用getifaddrs()獲取 ipv6 地址
- 可以通過線上手冊man getifaddrs瞭解詳細的關於getifaddrs函式的資訊;
- getifaddrs函式會建立一個本地網路介面的結構連結串列,該結構連結串列定義在struct ifaddrs中;
- 關於ifaddrs結構有很多文章介紹,本文僅簡單介紹一下與本文密切相關的內容,下面是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_dstaddr void *ifa_data; /* Address-specific data */ };
- ifa_next是結構連結串列的後向指標,指向連結串列的下一項,當前項為最後一項時,該指標為NULL;
- ifa_addr是本文主要用到的項,這是一個struct sockaddr, 看一下struct sockaddr的定義:
struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
- 實際上,當ifa_addr->sa_family為AF_INET時,ifa_addr指向struct sockaddr_in;當ifa_addr->sa_family為AF_INET6時,ifa_addr指向一個struct sockaddr_in6;
- sockaddr_in和sockaddr_in6這兩個結構同樣可以找到很多介紹文章,這裡就不多說了,反正這裡面是結構套著結構,要把思路捋順了才不至於搞亂;
- 下面是使用getifaddrs()獲取ipv6地址的源程式,可以看到,列印ipv6地址的那幾行,與上面的那個例子是一樣的;
#include#include#include#includeint 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()釋放掉連結串列所佔用的記憶體。
- 這個例子中,我們使用inet_ntop()將sin6_addr結構轉換成了字串形式的ipv6地址,還可以使用getnameinfo()來獲取ipv6的字串形式的地址;
- 可以通過線上手冊man getnameinfo瞭解getnameinfo()的詳細資訊;
- 下面是使用getifaddrs()獲取ipv6地址並使用getnameinfo()將將ipv6地址轉變為字串的源程式:
#include#include#include#include#includeint 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; }
- 和前面那個程式相比,這個程式增加了一個包含檔案netdb.h,這裡面有getnameinfo()的一些相關定義;
- 在這裡使用函式getnameinfo時,要明確ifa->ifa_addr指向的是一個struct sockaddr_in6,後面的常數NI_NUMERICHOST表示返回的主機地址為數字字串;
- 和上面的例子略有不同的是,使用getnameinfo獲取的ipv6地址的最後會使用‘%’連線一個網路介面的名稱,如下圖所示:
圖3:使用getnameinfo獲取ipv6地址
4. 使用 netlink 獲取 ipv6 地址
- netlink socket是使用者空間與核心空間通訊的又一種方法,本文並不討論netlink的程式設計方法,但給出了使用netlink獲取ipv6地址的源程式;
- 與上面兩個方法比較,使用netlink獲取ipv6地址的方法略顯複雜,在實際應用中並不多見,所以本文也就不進行更多的討論了;
- 下面是使用 netlink 獲取 ipv6 地址的源程式:
#include#include#include#include#include#include#includeint 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. 結語
- 本文給出了三種獲取ipv6地址的方法,均給出了完整的源程式;
- 本文對三種方法並沒有展開討論,以免文章冗長;
- 僅就獲取ipv6地址而言,前兩種方法比較常用而且簡單;
- 通常認為,使用者程式與核心通訊有四種方法:
- 系統呼叫
- 虛擬檔案系統(/proc、/sys等)
- ioctl
- netlink
- 本文所述的三個方法,正是使用了上述2、3、4三種方法;而獲取ipv6地址,簡單地使用系統呼叫無法實現。
(歡迎訪問我的部落格:https://whowin.gitee.io)