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地址而言,前兩種方法比較常用而且簡單;
  • 通常認為,使用者程式與核心通訊有四種方法:
  1. 系統呼叫
  2. 虛擬檔案系統(/proc、/sys等)
  3. ioctl
  4. netlink
  • 本文所述的三個方法,正是使用了上述2、3、4三種方法;而獲取ipv6地址,簡單地使用系統呼叫無法實現。

(歡迎訪問我的部落格:https://whowin.gitee.io)