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)

(0)

相关推荐

  • 如何让电脑自动获取IP地址或手动设置IP

    有时候电脑无法连接到网络可能是由于网络适配器没有正常工作或者电脑没有获取到唯一的IP地址所引起的,因此学会让电脑自动获取IP地址或手动设置IP十分重要,那么该如何操作呢? 操作方法 01 首先打开网络 ...

  • 如何设置DHCP(自动获取IP地址)

    DHCP是自动获取IP地址的意思,IP地址就像是一把门钥匙,如果设置不好网都连接不上的.DHCP的出现充分解决了这一个问题. 设置DHCP 01 打开[控制面板],到[网络与共享中心]那里,打开. 0 ...

  • Windows8系统中有线网卡自动获取IP地址设置步骤详细图解

    在局域网内,为了每个人都有自己的IP地址,但是有时候,还是会因为各种各样的原因致使IP冲突,并且,再不知道IP段的情况下,我们如何才能上网呢?这时候我们可以让电脑的有线网卡自动获取IP地址,这样既不不 ...

  • Win7有线网卡自动获取IP地址设置动画示范教程

    设置动画示范教程: Windows 7系统有线网卡自动获取IP地址的详细设置步骤如下: 第一步:鼠标点击电脑桌面右下角小电脑图标,在弹出的对话框中,点击 打开网络和共享中心。如下图所示: 第二步:弹出 ...

  • WinXP/Win7如何自动获取ip地址全程图解

    Windows XP 系统 ip 地址设置 第一步:右键点击桌面上的“网上邻居 ”,选择“属性 ” 第二步:右键点击“本地连接 ”,选择“属性 ” 注意:如果想配置无线网络ip,右键点击“无线网络连接 ...

  • Linux操作系统配置IPv6地址最简单的方法

    IPv6是"Internet Protocol Version 6"的缩写,也被称作下一代互联网协议,它是由IETF设计的用来替代现行的IPv4协议的一种新的IP协议。那么linux操作系统如何配置景 ...

  • Oracle VM Virtual中CentOS自动获取IP地址设置方法

    在CentOS配置网卡开机自动获取IP地址: vi /etc/sysconfig/network-scripts/ifcfg-eth0 将 ONBOOT="no" 改为 ONBOOT="yes" 保存 ...

  • windows系统手动配置ipv6地址(使用netsh)图文教程

    在XP,2003等早期版本中,ipv6地址在“网络连接”的属性配置里是无法手工配置的,只能使用netsh配置。配置方法如下: 首先,安装IPV6协议,ipv6 install 第二步,查看当前使用的本 ...

  • 电脑无法自动获取IP地址怎么办?

    故障排查: 由于笔者公司的客户机是通过DHCP服务器自动获取IP地址的,所以出现这种提示信息应该是客户机没有分配到IP地址所致。在“运行”中键入 “cmd”,进入“命令窗口”,输入“ipconfig” ...

  • 网卡无法获取IP地址自己给他找个

    电脑出现网卡无法获取IP地址出现这类问题,可以尝试更改网卡工作模式来解决。展开“设备管理器”中的“网络适配器”,然后双击网卡驱动标志,然后切换到 “高级”选项,在属性中将网卡工作速率模式由自动模式改为 ...