使用通常获取ipv4的IP地址的方法是无法获取ipv6地址的,本文介绍了使用C语言获取ipv6地址的三种方法:从proc文件从系统获取ipv6地址、使用getifaddrs()函数获取ipv6地址和使用netlink获取ipv6地址,每种方法均给出了完整的源程序,本文所有实例在 ubuntu 20.04 下测试通过,gcc 版本 9.4.0。

1. ipv4的IP地址的获取方法

  • 不论是获取 ipv4 的 IP 地址还是 ipv6 的地址,应用程序都需要与内核通讯才可以完成;

  • ioctl 是和内核通讯的一种常用方法,也是用来获取 ipv4 的 IP 地址的常用方法,下面代码演示了如何使用 ioctl 来获取本机所有接口的 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 无法获取 ipv6 地址,即便我们建立一个 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++;}
    }
    
  • 这段程序在我的机器上的运行结果是这样的:

    图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地址,32位16进制一组,中间没有:分隔符
    2 ifindex 接口设备号,每个设备都不同,按 16 进制显示
    3 prefixlen 16进制显示的前缀长度,类似 ipv4 的子网掩码的东西
    4 scopeid scope id
    5 flags 接口标志,这些标志标识着这个接口的特性
    6 devname 接口设备名称
  • 所以从这个文件中可以很容易地获得所有接口的 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 是一种不多见的用法,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_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 个参数指向一个 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 的数组;
    • 所以,实际上 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_dstaddrvoid            *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 <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() 释放掉链表所占用的内存。

  • 这个例子中,我们使用 inet_ntop() 将 sin6_addr 结构转换成了字符串形式的 ipv6 地址,还可以使用 getnameinfo() 来获取 ipv6 的字符串形式的地址;

  • 可以通过在线手册 man getnameinfo 了解 getnameinfo() 的详细信息

  • 下面是使用 getifaddrs() 获取 ipv6 地址并使用 getnameinfo() 将将 ipv6 地址转变为字符串的源程序

    #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;
    }
    
  • 和前面那个程序相比,这个程序增加了一个包含文件 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 <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. 结语

  • 本文给出了三种获取 ipv6 地址的方法,均给出了完整的源程序;
  • 本文对三种方法并没有展开讨论,以免文章冗长;
  • 仅就获取 ipv6 地址而言,前两种方法比较常用而且简单;
  • 通常认为,用户程序与内核通讯有四种方法
    1. 系统调用
    2. 虚拟文件系统(/proc、/sys等)
    3. ioctl
    4. netlink
  • 本文所述的三个方法,正是使用了上述 2、3、4 三种方法;而获取 ipv6 地址,简单地使用系统调用无法实现。

email: hengch@163.com

C语言如何获取ipv6地址相关推荐

  1. ar路由器 pppoe下发ipv6 dns_移动宽带如何获取IPV6地址

    都2020年了,为啥我家还没有IPV6!!! 事实上现今许多网络服务提供商(ISP)都提供了IPV6地址,你手机连上移动.联通.电信的基站,都能获取到一个IPV6地址,没道理宽带不给提供啊. 其实啊, ...

  2. 比较ipv6有状态与无状态地址分配工作特点_IPv6环境获取IPV6地址,无状态地址分配,一分钟了解下...

    一.DHCPv6基础 DHCPv6(Dynamic Host Configuration Protocol for IPv6,支持 IPv6 的动态主机配置协议)是针 对 IPv6 编址方案设计的,为 ...

  3. ipv6地址_「案例」路由器怎么自动获取IPv6地址?

    拓扑 规格 适用于V200R002C00及以上版本.所有形态的AR路由器. 组网需求 PC直连Router的接口后可通过协议自动获取IPv6地址,并自动生成默认网关,从而使PC与路由器可以自动连通.具 ...

  4. 【案例】路由器怎么自动获取IPv6地址?

    拓扑 规格 适用于V200R002C00及以上版本.所有形态的AR路由器. 组网需求 PC直连Router的接口后可通过协议自动获取IPv6地址,并自动生成默认网关,从而使PC与路由器可以自动连通.具 ...

  5. Redmi AC2100上使用Hiboy Padavan固件进行子网IPv6分配,Padavan子网无法获取IPv6地址

    1.基础设置 默认情况下Padavan连接到光猫以后,会自动获取并为默认eth3分配WAN口的IP地址,支持IPv6的光猫会获取IPv6地址给WAN口,但是不会继续对LAN中子网设备进行IPv6地址的 ...

  6. win10系统ipv6服务器地址,教你Win10系统重新获取IPV6地址的方法

    Win10系统如何重新获取IPV6地址?随着IPV4地址的枯竭,IPV6地址是未来的方向.现在已经有很多网络环境中在使用IPV6地址.最近发现Win10有的时候能获得IPV6的地址,有的时候无法获得I ...

  7. 高通SDX12:小网SIM卡QMI拨号无法获取IPv6地址问题的分析

    背景 客户反馈设备插小网卡驻网并加载qmi_wwan驱动后,使用多路拨号工具进行两路拨号,第一路无法获取IPv6地址,但是插现网卡测试是没有问题的.具体测试方法如下图: Check后只有第二路有PDN ...

  8. 如何通过c语言获取ipv6邻居表,急求在vc++6.0中获取IPV6地址的方法,高手请进,谢谢!!...

    本人最近getaddrinfo函数获取本机IP地址,本机的IPV4地址能正确获得,但本机IPV6地址只能获得环路地址,即::1.为什么不能获得IPV6的Global地址或链路本地地址.恳请指点,不胜感 ...

  9. PHP获取IPV6地址

    首先说几个坑: 我们之前获取IP的方式 private static function clientIp(){return getenv('HTTP_CLIENT_IP')?:getenv('HTTP ...

最新文章

  1. 云计算安全:技术与应用
  2. 昵图网学校计算机教室制度,ClassIn在线教室
  3. element中有多个合计_深入理解 Flutter 中的 Widget, Element, RenderObject
  4. python机器学习预测_使用Python和机器学习预测未来的股市趋势
  5. 通过ActiveX执行文件
  6. GB28181系统设计(三)-进程通信让python获取共享内存数据
  7. 加密解密你了解多少?
  8. Eclipse用法和技巧十三:自动生成的TODO注释1
  9. v-for key的作用
  10. Java:jdk8中map新增的merge方法
  11. msf win10漏洞_Kali对Windows2008/7的MS17010漏洞测试(MSF自带模块)
  12. 苹果三代耳机_关于苹果蓝牙耳机的一些事情
  13. 西门子s7-1200博图v16灌装机PLC程序+西门子KTP1200触摸屏程序 1200plc和3台v90伺服pn通讯
  14. Java版 猜数字小游戏
  15. OpenVINO 环境配置
  16. html5诅咒学园通关,NO.5 诅咒发作
  17. git error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)
  18. springboot实现网上宠物医院管理系统毕业设计
  19. 回调函数的注册和回调
  20. Python三次样条插值

热门文章

  1. hbuilder+dcloud开发APP
  2. codeblock找不到pthread
  3. iOS APP适配iPhone X全屏
  4. Briarcliff Entertainment 购买了关于开创性的非裔美国 NBA 球员的电视剧“Sweetwater”(独家)
  5. 中国好生意 经典论述:哈林是来主持的,刘欢是来开家长会的,那英是来唠嗑的,杨坤是来做宣传的,而......
  6. Linux(CentOS)搭建redmine项目管理系统
  7. CTF--web学习
  8. chall.tasteless.eu 中的注入题
  9. 'utf-8' codec can't decode byte 0xb5 in position : invalid start byte
  10. 聊聊这8种单例模式的优缺点