如何获取(Linux)机器的IP地址?

102
这个问题几乎与之前提出的如何获取本地计算机的IP地址?-问题相同。然而,我需要找到Linux机器的IP地址。因此:在C ++中以编程方式检测应用程序正在运行的Linux服务器的IP地址。 服务器将至少有两个IP地址,我需要一个特定的IP地址(给定网络中的公共IP地址)。我确信有一个简单的函数可以做到,但是在哪里呢?
为了让事情更清晰一些:
  • 服务器显然会有"localhost": 127.0.0.1
  • 服务器将拥有一个内部(管理)IP地址:172.16.x.x
  • 服务器将拥有一个外部(公共)IP地址:80.190.x.x
我需要找到外部IP地址来绑定我的应用程序。显然,我也可以绑定到INADDR_ANY(实际上这就是我目前所做的)。不过,我更喜欢检测公共地址。

3
为什么要标记ifconfig的popen?这确实是正确的做法。库会发生变化,但ifconfig和popen将永远存在。 - Erik Aronesty
3
不,ifconfigroute等命令已被弃用,现在应使用ip命令代替它们。请使用ip命令来进行相关操作。 - Anders
ifconfig仍然可以在比其他方法更多的架构上使用。因此,在适当的时候更改为ip命令。popen仍然是更好的解决方案。 - Erik Aronesty
12个回答

118

在OS X上,我发现ioctl解决方案存在问题(OS X符合POSIX标准,因此应该类似于Linux)。但是,getifaddress()可以让您轻松地完成相同的操作,在我的OS X 10.5上运行良好,下面的操作应该也是相同的。

我做了一个简单的示例,它将打印出机器的所有IPv4地址(您还应该检查getifaddrs是否成功,即返回0)。

我已更新它以显示IPv6地址。

#include <stdio.h>      
#include <sys/types.h>
#include <ifaddrs.h>
#include <netinet/in.h> 
#include <string.h> 
#include <arpa/inet.h>

int main (int argc, const char * argv[]) {
    struct ifaddrs * ifAddrStruct=NULL;
    struct ifaddrs * ifa=NULL;
    void * tmpAddrPtr=NULL;

    getifaddrs(&ifAddrStruct);

    for (ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
        if (!ifa->ifa_addr) {
            continue;
        }
        if (ifa->ifa_addr->sa_family == AF_INET) { // check it is IP4
            // is a valid IP4 Address
            tmpAddrPtr=&((struct sockaddr_in *)ifa->ifa_addr)->sin_addr;
            char addressBuffer[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN);
            printf("%s IP Address %s\n", ifa->ifa_name, addressBuffer); 
        } else if (ifa->ifa_addr->sa_family == AF_INET6) { // check it is IP6
            // is a valid IP6 Address
            tmpAddrPtr=&((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr;
            char addressBuffer[INET6_ADDRSTRLEN];
            inet_ntop(AF_INET6, tmpAddrPtr, addressBuffer, INET6_ADDRSTRLEN);
            printf("%s IP Address %s\n", ifa->ifa_name, addressBuffer); 
        } 
    }
    if (ifAddrStruct!=NULL) freeifaddrs(ifAddrStruct);
    return 0;
}

7
谢谢您提供这种好的方法,比那些乏味的ioctl更好!不过,您对IP6的处理仍然不正确-您应该转换为sockaddr_in6,即类似于tmpAddrPtr=&((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr;这样的方式。 - Andrey
2
@Andrey,谢谢。我已经更新了我的答案(虽然还没有测试过)。 - Twelve47
2
在我的情况下,它会给出“lo”和“wlan0”接口。我需要的是具有Internet连接的“wlan0”或“eth0”。那么,我应该使用“strcmp”还是有更好的方法? - Sudip Bhattarai

28
  1. 创建一个套接字。
  2. 执行 ioctl(<socketfd>, SIOCGIFCONF, (struct ifconf)&buffer);

阅读 /usr/include/linux/if.h 了解有关ifconfifreq结构的信息。这应该可以给您提供系统上每个接口的IP地址。还要阅读 /usr/include/linux/sockios.h获取其他的ioctl命令。


我原本以为它只会给你绑定接口的地址。 - Matt Joiner
@MattJoiner 不是的,请看一下手册页,SIOCGIFCONF 返回所有接口的信息。以下是手册页的一部分引用:“返回一个接口(传输层)地址列表。目前仅为了兼容性而言,这意味着只有 AF_INET(IPv4)家族的地址。用户将 ifconf 结构作为 ioctl 的参数传递。它包含一个指向 ifc_req 中 ifreq 结构数组的指针和其长度(以字节为单位)的 ifc_len。内核使用当前运行的所有 L3 接口地址填充 ifreqs。” - Stéphane

26

我喜欢jjvainio的答案。正如Zan Lnyx所说,它使用本地路由表来找到将用于与特定外部主机连接的以太网接口的IP地址。通过使用已连接的UDP套接字,您可以获取信息而不实际发送任何数据包。该方法要求您选择一个特定的外部主机。大多数时候,任何知名的公共IP地址都可以胜任。我喜欢Google的公共DNS服务器地址8.8.8.8用于此目的,但有时您可能想选择不同的外部主机IP。以下是说明完整方法的一些代码。

void GetPrimaryIp(char* buffer, size_t buflen) 
{
    assert(buflen >= 16);

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    assert(sock != -1);

    const char* kGoogleDnsIp = "8.8.8.8";
    uint16_t kDnsPort = 53;
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_addr.s_addr = inet_addr(kGoogleDnsIp);
    serv.sin_port = htons(kDnsPort);

    int err = connect(sock, (const sockaddr*) &serv, sizeof(serv));
    assert(err != -1);

    sockaddr_in name;
    socklen_t namelen = sizeof(name);
    err = getsockname(sock, (sockaddr*) &name, &namelen);
    assert(err != -1);

    const char* p = inet_ntop(AF_INET, &name.sin_addr, buffer, buflen);
    assert(p);

    close(sock);
}

2
这给了我私有IP 127...*。可能是因为我在NAT后面? - eugene

15

您已经发现了没有单一的“本地IP地址”。以下是如何找到可以发送到特定主机的本地地址。

  1. 创建一个UDP套接字
  2. 将套接字连接到外部地址(最终将接收本地地址的主机)
  3. 使用getsockname获取本地地址

我以前从没这么做过,但我喜欢这种方法。它会自动使用操作系统的路由表,比扫描接口列表要简单得多。 - Zan Lynx
5
假定您知道外部地址是什么。在数据中心中,通常并非如此。 - Christopher
你可以尝试使用分配给不同大陆的三个(IANA使这很容易),并接受其中最好的两个。 - Joshua

9

这种方法的优点是可以在许多Unix版本上运行...并且您可以轻松修改它以在任何操作系统上工作。以上所有解决方案都会在不同阶段出现编译器错误。一旦有了一个好的POSIX方式来解决,就不要使用此方法(在撰写本文时还没有这种方法)。

// ifconfig | perl -ne 'print "$1\n" if /inet addr:([\d.]+)/'

#include <stdlib.h>

int main() {
        setenv("LANG","C",1);
        FILE * fp = popen("ifconfig", "r");
        if (fp) {
                char *p=NULL, *e; size_t n;
                while ((getline(&p, &n, fp) > 0) && p) {
                   if (p = strstr(p, "inet ")) {
                        p+=5;
                        if (p = strchr(p, ':')) {
                            ++p;
                            if (e = strchr(p, ' ')) {
                                 *e='\0';
                                 printf("%s\n", p);
                            }
                        }
                   }
              }
        }
        pclose(fp);
        return 0;
}

这在非英语的Linux系统上无法工作,例如在德语的Debian系统中,“inet addr”的输出是“inet Adresse”。 - Martin Meeser
我的一位 Linux 同事刚刚给了我这个命令:/sbin/ifconfig eth1 | grep \"inet addr\" | cut -d':' -f2 | cut -d' ' -f1。当然,这只是将“解析”移到了控制台,语言问题仍然存在。 - Martin Meeser
1
@MartinMeeser:通过分别查找“inet”和“:”,使其与语言无关。 - Erik Aronesty
1
然后你有其他语言,它们甚至不是基于拉丁字符集的。 解决方案是不要使用命令行工具,因为它们像 ifconfig 一样会过时。现在应该使用 ip 命令。生成语言中立输出的正确方法是将 LANG 环境变量设置为 C,例如:LANG=C ls -l - Anders
1
谢谢。这是一个简单而棒极了的解决方案! - Ahmad Afkande
显示剩余7条评论

6

我认为你的问题并没有一个明确的正确答案。相反,有很多方法可以接近你想要的结果。因此,我将提供一些提示来完成它。

如果机器有超过2个接口(lo算一个),你将难以轻松地自动检测到正确的接口。以下是如何解决此问题的一些方法。

例如,问题在于,如果主机位于NAT防火墙后面的DMZ中,该防火墙将公共IP更改为某些私有IP并转发请求。您的机器可能有10个接口,但只有一个对应于公共接口。

即使自动检测也无法在双重NAT的情况下工作,其中您的防火墙甚至将源IP转换为完全不同的内容。因此,您甚至不能确定默认路由是否通向具有公共接口的接口。

通过默认路由进行检测

这是我推荐的自动检测方式

类似 ip r get 1.1.1.1 的命令通常会告诉您具有默认路由的接口。

如果您想在喜欢的脚本/编程语言中重新创建此操作,请使用 strace ip r get 1.1.1.1 并遵循黄砖路。

使用/etc/hosts进行设置

如果您想保持控制,这是我的建议

您可以在 /etc/hosts 中创建一个条目,例如:

80.190.1.3 publicinterfaceip

然后,您可以使用别名publicinterfaceip来引用您的公共接口。

不幸的是,haproxy无法理解IPv6的这个技巧。

使用环境变量

如果您不是root用户,则环境变量是/etc/hosts的一个很好的解决方法。

/etc/hosts相同,但使用环境变量。 您可以尝试/etc/profile~/.profile

因此,如果您的程序需要一个变量MYPUBLICIP,那么您可以包含以下代码(这是C语言,请随意从中创建C ++):

#define MYPUBLICIPENVVAR "MYPUBLICIP"

const char *mypublicip = getenv(MYPUBLICIPENVVAR);

if (!mypublicip) { fprintf(stderr, "please set environment variable %s\n", MYPUBLICIPENVVAR); exit(3); }

所以你可以这样调用你的脚本/程序/path/to/your/script

MYPUBLICIP=80.190.1.3 /path/to/your/script

这甚至在crontab中也有效。

枚举所有接口并排除您不想要的接口

如果您不能使用ip,那么绝望的方法

如果您知道您不想要什么,您可以枚举所有接口并忽略所有错误的接口。

这里似乎已经有一个答案https://dev59.com/c3VC5IYBdhLWcg3wsTVi#265978支持这种方法。

像DLNA一样操作

喝醉了试图在酒精中淹死自己的方法

您可以尝试枚举网络上的所有UPnP网关,以此找到某些“外部”事物的合适路线。 这甚至可能在您的默认路由未指向的路径上。

更多信息,请参见https://en.wikipedia.org/wiki/Internet_Gateway_Device_Protocol

这将为您提供良好的印象,哪个是真正的公共接口,即使您的默认路线指向其他位置。

还有更多

山与先知相遇的地方

IPv6路由器会广告自己,以便为您提供正确的IPv6前缀。 查看前缀会让您了解它是否具有某些内部IP或全局IP。

您可以监听IGMP或IBGP帧,以找出一些合适的网关。

不到2 ^ 32个IP地址。 因此,在LAN上仅对它们进行ping不需要长时间。 这为您提供了有关互联网从您的角度来看位于何处的统计提示。 但是,您应该比著名的https://de.wikipedia.org/wiki/SQL_Slammer更加明智。

ICMP甚至ARP是网络旁路信息的良好来源。 它也可能对您有所帮助。

您可以使用以太网广播地址与所有网络基础设施设备通信,这通常会有所帮助,例如DHCP(甚至DHCPv6)等。

这个附加列表可能是无限的,并且总是不完整的,因为每个网络设备制造商都在忙于发明新的安全漏洞,以自动检测自己的设备。 这通常对如何检测不应该有公共接口的地方有很大帮助。

说得够多了。 出门。


5

除了Steve Baker所说的内容外,您可以在netdevice(7)手册页中找到有关SIOCGIFCONF ioctl的描述。

一旦您获得主机上所有IP地址的列表,您将不得不使用特定于应用程序的逻辑来过滤掉您不想要的地址,并希望仅剩下一个IP地址。


4

不要硬编码:这种东西是会变化的。许多程序通过读取配置文件来确定绑定什么,并执行文件中指定的操作。这样,如果您的程序将来需要绑定到非公共 IP 的内容,它可以轻松完成。


2
由于问题指定了Linux,我发现使用netlink是发现计算机IP地址的最佳技术。通过创建一个协议为NETLINK_ROUTE的netlink套接字,并发送RTM_GETADDR,您的应用程序将收到包含所有可用IP地址的消息。这里提供了一个示例(链接)
为了简化消息处理的某些部分,libmnl很方便。如果您想了解更多关于NETLINK_ROUTE的不同选项(以及它们如何被解析)的信息,最好的来源是iproute2的源代码(特别是监视应用程序)以及内核中的接收函数。 rtnetlink的手册也包含有用的信息。

1

你可以只使用 stdio.h 头文件来进行编程吗?(不涉及网络头文件)

以下是完整的 C++ 代码:

(如果你使用家庭调制解调器,则此代码适用于本地 IP)

#include <stdio.h>
int main()
{
    static char ip[32];
    FILE *f = popen("ip a | grep 'scope global' | grep -v ':' | awk '{print $2}' | cut -d '/' -f1", "r");
    int c, i = 0;
    while ((c = getc(f)) != EOF) i += sprintf(ip+i, "%c", c);
    pclose(f);
    printf(ip);
}
  • BONUS: 你也可以使用它的 NDK C++ 编译器将其交叉编译到 Android 上

  • JUST SAYING: 我有一个 VPS,在那里我运行了这个程序并获取了外部 IP


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接