前言

本文接着上文的内容,主要解答上文留下的疑问:既然不能使用​​InetAddress#getLocalHost()​​直接去获取到本机的IP地址,那么如何破呢?

本文将介绍的是一种通用的获取本机IP地址的解决方案,也就是所谓的“正确姿势”。


正文

为了更好的解释为何​​InetAddress#getLocalHost()​​不靠谱,有必要先普及下Linux下的相关文件的解释,比较我们的Java应用绝大部分情况下都是跑在Linux环境下的(甚至国内基本都跑在centos上吧)。


Linux下/sysconfig/network、hosts、host.conf、resolv.conf文件解释

这几个文件都位于​​/etc/​​目录下,下面分别给出解释和示例:

  • ​/sysconfig/network​​:此文件是针对本计算机的,是给计算机起的一个名字,是计算机的一个标识。可以使用​​uname -n​​​ 命令来查看本地计算机的计算机名称(比如本例我使用该命令得到的值是​​l-xxx.syc.prod.ali.qr​​,也就是配置里的HOSTNAME的值)
# 支持网络通信
NETWORKING=yes
# 主机名,若你不设置默认是localhost.localdomain
# 使用hostname命令查看到的就是这个值
HOSTNAME=l-xxx.syc.prod.ali.qr
# 不启用IPV6的支持
NETWORKING_IPV6=no
PEERNTP=no
  • ​hosts​​:不同于network,此文间是在网络上使用的。和Windows系统下的hosts文件相类似,就是一个文本文件,里面存放一些IP和​​域名​​的对应关系(注意:都是先ip,后域名)
127.0.0.1   localhost 
# ::1表示的时ipv6的本地地址,也就是0000:0000:0000:0000:0000:0000:0000:0001
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
# 内网ip地址 -> 主机名 的映射关系
10.102.1.153 l-xxx.syc.prod.ali.qr
  • ​host.conf​​:解析顺序/方式,属于一种规则配置。
# 这里规定先使用DNS来解析域名,然后再查询“/etc/hosts”文件(也可以相反,你调个顺序即可)
order bind,hosts
# 指定是否“/etc/hosts”文件中指定的主机可以有多个地址
multi on
resolv.conf

DNS

服务器

的配置文件

,用于设置DNS服务器的IP地址及DNS域名,还包含了主机的域名搜索顺序。该文件由

域名解析

来读取(下面会有Java代码示例)

  • 格式:每行以一个关键字开头,后接一个或多个由空格隔开的参数
  • 可选的关键字有:
  • ​nameserver​​:(最重要,此关键字必选,其它可选)定义DNS服务器的IP地址,可以有很多行的nameserver,每一个带一个IP地址。在查询时就按nameserver在本文件中的顺序进行,且只有当第一个nameserver没有反应时才查询下面的nameserver。
  • ​domain​​:定义本地域名/主机的域名
search

:定义域名的搜索列表

  • search和domain不能共存,如果同时存在,以最后出现的为准。
  • ​sortlist​​​:对返回的域名进行排序,如​​sortlist 130.155.160.0/255.255.240.0 130.155.0.0​
options

:用于配置resolver的内置变量,不是resolv.conf的常见配置。语法如下:

options [option] ...
  • ndots:[n]:设置调用res_query()解析域名时域名至少包含的点的数量
  • timeout:[n]:设置等待dns服务器返回的超时时间,单位秒。默认值5
  • attempts:[n]:设置resolver向DNS服务器发起域名解析的请求次数。默认值2
# 一般是内网DNS服务器地址,这样就可以解析你的内网域名了喽
nameserver 202.102.192.68
nameserver 202.102.192.69
search qq.com baidu.com
options no-check-names
options attempts:1
options timeout:1

了解了这些概念后,接下来就得认识​​InetAddress​​这个API了。本文将会演示它在windows上(本机)以及Linux下运行的案例,会有差异,请注意区分。


直接使用getLocalHost()获取本机IP的错误示例

标题已经很明显指出了:这是错误示例。我相信绝大部分小伙伴获取本机IP都后悔这么用:

public static void main(String[] args) throws UnknownHostException {
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost.getHostAddress());
}

运行程序,在不同的操作系统上值可能还不一样:

  • 在windows上:
192.168.199.175
  • 在windows上看似正常。但是但是但是,请你开一个v再运行试一下,定会让你大跌眼镜(比如我开启公司的v后,输出的值是​​2.0.0.137​​)
  • 可以看到当出现多个网卡接口工作时,windows可能就不好使了,而多个网卡同时工作的情况是很正常的(比较虚拟网卡经常很多)
  • 在Linux上:
127.0.0.1
  • what?

为何在Linux下请你一定不要使用它来获取本机IP,因为它就是简单的读取​​/etc/hosts​​​的内容,所以它默认返回的是​​127.0.0.1​​非常的不靠谱,因此本方法十分不建议在生产上使用

​/etc/hosts​​​的第一行一般均是:​​127.0.0.1 localhost​​​,所以返回值是​​127.0.0.1​​​(倘若你把第一行改为​​127.1.1.1 localhost​​​,那么它的返回值就是​​127.1.1.1​​了)


获取本机IP地址的正确姿势

关于获取本机地址的正确姿势,主要分为如下两种场景展开说明。

简单情况(废弃)

简单情况下,就可以通过​​InetAddress.getLocalHost()​​来获取到本机ip地址。注意这里的关键词:简单。因此它对环境是有要求的:

  1. windows环境
  2. 非多网卡协同工作环境(比如不能开启v**)

很明显,这种“简单”情况在实际生产中并不存在,因此仅限yy,不可使用。

复杂情况(通用,推荐的方案)

它是一种通用方案,推荐使用在实际的代码中。

public static void main(String[] args) throws UnknownHostException {
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost.getHostAddress());

System.out.println("----------------下面才是正确的获取方式----------------");
localHost = getLocalHostExactAddress();
System.out.println(localHost.getHostAddress());
// System.out.println(localHost.getHostName());
}


public static InetAddress getLocalHostExactAddress() {
try {
InetAddress candidateAddress = null;

Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface iface = networkInterfaces.nextElement();
// 该网卡接口下的ip会有多个,也需要一个个的遍历,找到自己所需要的
for (Enumeration<InetAddress> inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) {
InetAddress inetAddr = inetAddrs.nextElement();
// 排除loopback回环类型地址(不管是IPv4还是IPv6 只要是回环地址都会返回true)
if (!inetAddr.isLoopbackAddress()) {
if (inetAddr.isSiteLocalAddress()) {
// 如果是site-local地址,就是它了 就是我们要找的
// ~~~~~~~~~~~~~绝大部分情况下都会在此处返回你的ip地址值~~~~~~~~~~~~~
return inetAddr;
}

// 若不是site-local地址 那就记录下该地址当作候选
if (candidateAddress == null) {
candidateAddress = inetAddr;
}

}
}
}

// 如果出去loopback回环地之外无其它地址了,那就回退到原始方案吧
return candidateAddress == null ? InetAddress.getLocalHost() : candidateAddress;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

在windows运行输出(即使你开了v**都木有关系的哦):

2.0.2.81
----------------下面才是正确的获取方式----------------
192.168.199.175

在Linux下运行输出:

127.0.0.1
----------------下面才是正确的获取方式----------------
10.102.11.29

可以看到通过这种方式获取本机IP,不管是windows or Linux,亦不管是开启了v**与否,最终均能得到正确的结果。




最后最为辅助材料,介绍下​​InetAddress​​的其它方法们。

InetAddress的其它实例方法们介绍

//获取主机名
public String getHostName();
//获取主机名(每次方法都解析DNS去获取主机名,而不从缓存中获取)
public String getCanonicalHostName();
//获取以字节数组形式的ip地址。
public byte[] getAddress();
//获取点分四段式ip。
public String getHostAddress();

//可以根据指定timeout时间内测试网络是否可达,是否可以建立连接。
public boolean isReachable(int timeout);
//可以指定从本机那个网卡、生存时间ttl 和指定时间来测试网络是否可达。
public boolean isReachable(NetworkInterface netif, int ttl, int timeout);

// 当IP地址是广播地址(MulticastAddress)时返回true,否则返回false。
// 通过广播地址可以向网络中的所有计算机发送信息,而不是只向一台特定的计算机发送信息。
// IPv4的广播地址的范围是224.0.0.0 ~ 239.255.255.255
// IPv6的广播地址第一个字节是FF 其他的字节可以是任意值。
public boolean isMulticastAddress();
// 当IP地址是通配符地址时返回true,否则返回false。
// IPv4的通配符地址是0.0.0.0
// IPv6的通配符地址是0:0:0:0:0:0:0:0,也可以简写成::。
public boolean isAnyLocalAddress();
// 当IP地址是loopback地址时返回true,否则返回false。loopback地址就是代表本机的IP地址。
// IPv4的loopback地址的范围是127.0.0.0 ~ 127.255.255.255,也就是说,只要第一个字节是127,就是lookback地址。
// IPv6的loopback地址是0:0:0:0:0:0:0:1,也可以简写成::1
public boolean isLoopbackAddress();
// 当IP地址是本地连接地址(LinkLocalAddress)时返回true,否则返回false。
// IPv4的本地连接地址的范围是169.254.0.0 ~ 169.254.255.255。
// IPv6的本地连接地址的前12位是FE8,其他的位可以是任意取值,如FE88::和FE80::ABCD::都是本地连接地址。
public boolean isLinkLocalAddress();
// 当IP地址是地区本地地址(SiteLocalAddress)时返回true,否则返回false。(是不是内网ip)
// IPv4的地址本地地址分为三段:10.0.0.0 ~ 10.255.255.255、172.16.0.0 ~ 172.31.255.255、192.168.0.0 ~ 192.168.255.255
// (企业内部或个人内部的局域网内部的ip都应该在此三个网段内 因为内网用ipv4足够)
// IPv6的地区本地地址的前12位是FEC,其他的位可以是任意取值,如FED0:: 和 FEF1:: 都是地区本地地址。
public boolean isSiteLocalAddress();


// 当IP地址是全球范围的广播地址时返回true,否则返回false。全球范围的广播地址可以向Internet中的所有的计算机发送信息。
// IPv4的广播地址除了224.0.0.0和第一个字节是239的IP地址都是全球范围的广播地址
// IPv6的全球范围的广播地址中第一个字节是FF,第二个字节的范围是0E ~ FE,其他的字节可以是任意值,如FFBE::、FF0E::都是全球范围的广播地址。
public boolean isMCGlobal();
// 当IP地址是本地接口广播地址时返回true,否则返回false。
// 本地接口广播地址不能将广播信息发送到产生广播信息的网络接口,即使是同一台计算机的另一个网络接口也不行。
// 所有的IPv4广播地址都不是本地接口广播地址。IPv6的本地接口广播地址的第一个字节是FF,第二个节字的范围是01 ~ F1,
// 其他的字节可以是任意值,如FFB1::、FF01:A123::都是本地接口广播地址。
public boolean isMCNodeLocal();
// 当IP地址是子网广播地址时返回true,否则返回false。
// IPv4的子网广播地址的范围是224.0.0.0 ~ 224.0.0.255。
// IPv6的子网广播地址的第一个字节是FF,第二个字节的范围是02 ~ F2,其他的字节可以是任意值,如FFB2:: 和 FF02:ABCD:: 都是子网广播地址。
public boolean isMCLinkLocal();
// 当IP地址是站点范围的广播地址时返回true,否则返回false。
// 使用站点范围的广播地址,可以向站点范围内的计算机发送广播信息。
// IPv4的站点范围广播地址的范围是239.255.0.0 ~ 239.255.255.255,如239.255.1.1、239.255.0.0都是站点范围的广播地址。
// IPv6的站点范围广播地址的第一个字节是FF,第二个字节的范围是05 ~ F5,其他的字节可以是任意值,如FF05:: 和 FF45:: 都是站点范围的广播地址。
public boolean isMCSiteLocal();
// 当IP地址是组织范围的广播地址时返回ture,否则返回false。
// 使用组织范围广播地址可以向公司或企业内部的所有的计算机发送广播信息。
// IPv4的组织范围广播地址的第一个字节是239,第二个字节不小于192,第三个字节不大于195,如239.193.100.200、239.192.195.0都是组织范围广播地址。
// IPv6的组织范围广播地址的第一个字节是FF,第二个字节的范围是08 ~ F8,其他的字节可以是任意值,如FF08:: 和 FF48::都是组织范围的广播地址。
public boolean isMCOrgLocal();

总结

关于​​InetAddress​​的所有内容就介绍到这,相信经过此两篇内容的讲解后,你对Java处理域名解析,以及获取本地IP地址时将不会再胆怯,干吧。另外,其实上Java该有更高级的功能:

  • 自己指定DNS地址,形如这样:
// 这么一来我们的hostName域名都将交给`114.114.114.114`去帮我们完成解析
System.setProperty("sun.net.spi.nameservice.nameservers", "114.114.114.114");
  • 自己指定DNS服务的提供者:
// 若你没指定此key,那就是default。默认就会使用系统自带的DNS
// `dns,sun`的意思是:会使用`sun.net.spi.nameservice.nameservers`配置指定的DNS来解析
System.setProperty("sun.net.spi.nameservice.provider.1", "dns,sun");

这些内容几乎很少会涉及到,因此本文并没必要展开。


参考链接:

​https://cloud.tencent.com/developer/article/1610919​