网络编程-获取本机IP及MAC的几种方式

Posted by 周思进 on February 18, 2021

一、通过 gethostbyname 获取本机IP(支持IPV4和IPV6,但已不推荐)

具体代码如下:

int get_local_ip(void)
{
    char hostname[64] = {0};
    struct hostent *p_host_entry = NULL;
    int ret = -1;
    int i = 0;
    char str_ip6[64] = {0};

    ret = gethostname(hostname, sizeof(hostname));
    if (ret != 0)
    {
        printf("gethostname failed.\n");
        return -1;
    }

    p_host_entry = gethostbyname(hostname);
    if (NULL == p_host_entry)
    {
        printf("gethostbyname failed.\n");
        return -1;
    }

    while (p_host_entry->h_addr_list[i] != NULL)
    {
        printf("ip:%s\n",inet_ntoa(*(struct in_addr *)p_host_entry->h_addr_list[i++]));
    }

    // 如果要获取 ipv6 地址,需要使用 gethostbyname2 接口
    p_host_entry = gethostbyname2(hostname, AF_INET6);
    if (NULL == p_host_entry)
    {
        printf("gethostbyname2 failed.\n");
        return -1;
    }

    i = 0;
    while (p_host_entry->h_addr_list[i] != NULL)
    {
        memset(str_ip6, 0, sizeof(str_ip6));
        inet_ntop(AF_INET6, (struct in6_addr *)p_host_entry->h_addr_list[i++], str_ip6, sizeof(str_ip6));
        printf("str_ip6:%s\n", str_ip6);
    }

    return 0;
}

先通过 gethostname 接口获取主机名,然后再通过 gethostbyname 根据获取的主机名来获取本机相关 IP 地址。

其中 gethostbyname 只支持IPV4,gethostbyname2 可支持IPV6。

不过如 man 手册下面说的那样

Historically, passing a host’s own hostname to gethostbyname() or gethostbyname2() has been a popular technique for determining that host’s IP address(es), but this is fragile, and doesn’t work reliably in all cases. The appropriate way for software to discover the IP address(es) of the host it is running on is to use getifaddrs(3).

通过 gethostbyname 接口来获取 IP 并不适用于所有场景,比如无法直接获取指定网口地址,更推荐使用 getifaddrs 接口。

同样如 man 手册所述

The gethostbyname() and gethostbyaddr() functions are obsolete. Applications should use getaddrinfo(3) and getnameinfo(3) instead

应用代码都不应该使用 gethostbyname 和 gethostbyaddr 之类的接口了,而应该使用 getaddrinfo 和 getnameinfo 接口。


二、通过 getifaddrs 获取本机 IP(支持IPV4和IPV6) 、子网掩码及 MAC

下面代码根据 man 手册示例做了点修改,如下:

int get_local_ip(char *interface, int family)
{
    struct ifaddrs *ifaddr = NULL, *ifa = NULL;
    struct sockaddr_ll *s = NULL;
    int ret = -1;
    char host[NI_MAXHOST] = {0};
    int i = 0;

    // 入参判断

    if (getifaddrs(&ifaddr) == -1) {
        perror("getifaddrs");
        return -1;
    }

    /* Walk through linked list, maintaining head pointer so we
        can free list later */
    for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
        if (ifa->ifa_addr == NULL)
            continue;

        if (family == ifa->ifa_addr->sa_family
            && (0 == strcmp(interface, ifa->ifa_name)))
        {
            if (family == AF_PACKET)
            {
                s = (struct sockaddr_ll*)ifa->ifa_addr;
                for (i=0; i < s->sll_halen; i++)
                {
                    printf("%02x%c", (s->sll_addr[i]), (i+1!=s->sll_halen)?':':'\n');
                }
            }
            else
            {
                ret = getnameinfo(ifa->ifa_addr,
                        (family == AF_INET) ? sizeof(struct sockaddr_in) :
                                                sizeof(struct sockaddr_in6),
                        host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
                if (ret != 0) {
                    printf("getnameinfo() failed: %s\n", gai_strerror(ret));
                    freeifaddrs(ifaddr);
                    return -1;
                }
                printf("\taddress: <%s>\n", host);
            }
        }
    }

    freeifaddrs(ifaddr);
    return 0;
}

通过 getifaddrs 接口,就可以根据指定网口名来获取想要的地址,同时支持 IPV4 和 IPV6 地址的获取。除了获取 IP 地址,也可以获取子网掩码。

可以在上面基础上对接口进行封装修改,分别单独用于外部获取 IPV4 地址、IPV6 地址及 MAC 地址,就是需要注意,如果是获取的 IPV6 地址,则一个网口是可以存在多个IPV6地址的。


三、通过 ioctl 获取本机 IP(只支持IPV4)、子网掩码及MAC

具体代码如下:

int get_local_ip(char *interface)
{
    int fd = -1;
    struct ifreq ifr;
    int ret = -1;
    unsigned char mac[6] = {0};

    // 入参检查

    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == fd)
    {
        printf("socket failed.\n");
        return -1;
    }

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_addr.sa_family = AF_INET;
    strncpy(ifr.ifr_name, interface, IFNAMSIZ-1);

    (void)ioctl(fd, SIOCGIFADDR, &ifr);
    printf("ip:%s\n", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr));

    (void)ioctl(fd, SIOCGIFNETMASK, &ifr);
    printf("ip:%s\n", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_netmask)->sin_addr));

    (void)ioctl(fd, SIOCGIFHWADDR, &ifr);
    memcpy(mac, ifr.ifr_hwaddr.sa_data, 6);
    printf("mac:%02x:%02x:%02x:%02x:%02x:%02x\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

    close(fd);
    return 0;
}

如 man 手册 netdevice(7) 所述

Return a list of interface (transport layer) addresses. This currently means only addresses of the AF_INET (IPv4) family for compatibility

ioctl 只能用于获取 ipv4 地址,如果要获取 ipv6 地址,只能使用 getifaddrs 接口,或者读取 /proc/net/if_inet6 文件。

也因此 socket 的第一个选项固定设置成 AF_INET,对于第二个选项,即可以使用 SOCK_STREAM 也可以使用 SOCK_DGRAM。

此方法可以得到很多网口信息,如子网掩码(SIOCGIFNETMASK),MAC 地址(SIOCGIFHWADDR),MTU ,网口的状态等等。

对于 SIOCGIFCONF 选项,是将本系统的所有网口信息都获取上来,具体代码如下:

int get_local_ip()
{
    int fd = -1;
    struct ifreq *p_ifr = NULL;
    unsigned char mac[6] = {0};
    struct ifconf ifc;
    char buf[1024] = {0};

    // 入参检查

    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == fd)
    {
        printf("socket failed.\n");
        return -1;
    }

    memset(&ifc, 0, sizeof(ifc));
    ifc.ifc_len = sizeof(buf);
    ifc.ifc_buf = buf;

    if (-1 == ioctl(fd, SIOCGIFCONF, &ifc))
    {
        close(fd);
        return -1;
    }

    for (p_ifr = ifc.ifc_req; p_ifr != ifc.ifc_buf+ifc.ifc_len; p_ifr++)
    {
        printf("if_name:%s\n", p_ifr->ifr_name);
        printf("ip:%s\n", inet_ntoa(((struct sockaddr_in *)&p_ifr->ifr_addr)->sin_addr));
        memcpy(mac, p_ifr->ifr_hwaddr.sa_data, 6);
        printf("mac:%02x:%02x:%02x:%02x:%02x:%02x\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }

    close(fd);
    return 0;
}


四、通过 getsockname 获取本机 IP

如果是和对端已连接的套接字,则可以通过 getpeername 获取对端 IP,通过 getsockname 接口获取本端 IP。


五、通过 curl 获取本机公网 IP

如果是要获取本机的公网iP地址,可以通过如下命令获取

curl ifconfig.me