背景
在某些场景下,我们可能需要在Java中判断到某个主机的网络是否连通,比如我们的系统中可能有业务需要录入一些主机信息,此时为了更好的用户体验,我们可能会在前端页面上提供一个拨测按钮,让用户可以在输入主机地址之后进行连通性检验,来判断我们的系统和目标主机是否网络可达,同时也能一定程度上保证用户输入的主机地址有效性。
这只讨论使用最简单且通用的方式判断,如果我们清楚目标主机/应用有其他可用的连通性测试接口,那么使用这种更准确的接口当然是更好的解决方案。比如若要测数据库连通性,我们可以尝试建立一个数据库连接来判断,这样更加准确
提到这种简单的连通性测试,有计算机基础的同学肯定会想到ping
命令,如果我们借助ping
命令来检测目的主机的网络连通性,则Java代码可以这样写:
java
public static void main(String[] args) throws Exception {
String ip = "192.168.121.136";
linuxPing(ip);
}
/**
* Linux平台下的Ping,使用Runtime来执行命令。Windows中ping参数不太一样,需要对应调整
*/
public static void linuxPing(String ip) throws IOException, InterruptedException {
Process exec = Runtime.getRuntime().exec("ping -c 4 " + ip);
BufferedReader reader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
//解析每行输出,判断最终结果
}
}
上述实现方式确实也是一种方案,但需要考虑几个跨平台带来的问题:
- Windows和Linux下的
ping
命令选项参数不一致,如指定次数时Windows中用-n
选项,而Linux中用-c
选项。需要判断平台,并使用对应的ping
命令 - IPv6和IPv4使用的
ping
命令选项可能不一致(如Linux中ping
命令在使用IPv6地址时应该为ping -6 xxx
,Windows中可不指定),因此在实现时需要区分不同类型的地址 - 不同平台下
ping
命令的输出格式也不一定相同,如果使用命令的输出来解析结果,可能需要适配不同的格式
当然,这种常用功能想必早已经有开源成熟的解决方案,我们自己去实现属实有些许费力不讨好的意味。这时我们会发现,Java中早就给我们提供了InetAddress.isReachable()
方法来完成这个任务,于是我们自信地写出以下代码:
java
public static void main(String[] args) throws Exception {
String ip;
if (args != null && args.length > 0) {
ip = args[0];
} else {
ip = "192.168.121.136";
}
InetAddress address = null;
try {
address = InetAddress.getByName(ip);
if (address.isReachable(5 * 1000)) {
System.out.println(ip + " is reachable");
} else {
System.out.println(ip + " is not reachable");
}
} catch (IOException e) {
System.out.println("exception: " + e.getMessage());
}
}
上述代码使用了Java标准库方法,跨平台、IPv4/IPv6区分等问题我们就通通不用考虑了,只需要把异常处理一下即可。但代码上线时,问题又双叒叕来了------明明自测还好好的,怎么上线全部连通性测试都失败了??
现象
当我们使用上述代码进行测试时,会发现一种奇怪的现象------使用ping
命令能ping
通,但用InetAddress.isReachable()
却老是不行
查阅网上的资料,有的回答中提到了一个点,大概意思是:
使用isReachable()时,如果应用的权限不足,可能会导致isReachable()检测连通性失败,始终返回false
确实,我们的应用程序通常是不会以root等超级用户身份执行的,上图测试失败时我们就是以普通用户longqinx
执行的,那换成root试试呢?
果然!当我们切换到root用户时,isReachable()
开始正常工作了;再次尝试用普通用户longqinx
执行时,发现isReachable()
又失败了...
注:上述示例中,
sudo -u username command
表示以username
用户身份执行command
命令
问题原因
在网络上查了一圈儿资料之后,多数人都提到了权限问题和防火墙问题,但却几乎无人再去深入研究这个问题的根本原因所在。我心中的两个问题始终没有得到解答:
isReachable()
到底是干了啥才需要高级权限?- 又是哪里和防火墙扯上了关系?
源代码才是最终的答案!于是我开始跟踪InetAddress.isReachable()
的调用链,发现其最终调用了一个名为java.net.Inet4AddressImpl#isReachable0
的Native方法
继续跟进源码,最终在OpenJDK的源码中找到了这个isReachable0
的实现逻辑:
c
/*
* Class: java_net_Inet4AddressImpl
* Method: isReachable0
* Signature: ([bI[bI)Z
*/
JNIEXPORT jboolean JNICALL
Java_java_net_Inet4AddressImpl_isReachable0(JNIEnv *env, jobject this,
jbyteArray addrArray, jint timeout,
jbyteArray ifArray, jint ttl)
{
//.....省略......
// Let's try to create a RAW socket to send ICMP packets.
// This usually requires "root" privileges, so it's likely to fail.
fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (fd == -1) {
return tcp_ping4(env, &sa, netif, timeout, ttl);
} else {
// It didn't fail, so we can use ICMP_ECHO requests.
return ping4(env, fd, &sa, netif, timeout, ttl);
}
}
/**
* ping implementation using tcp port 7 (echo)
*/
static jboolean
tcp_ping4(JNIEnv *env, SOCKETADDRESS *sa, SOCKETADDRESS *netif, jint timeout, jint ttl);
/**
* ping implementation.
* Send an ICMP_ECHO_REQUEST packet every second until either the timeout
* expires or an answer is received.
* Returns true if an ECHO_REPLY is received, false otherwise.
*/
static jboolean
ping4(JNIEnv *env, jint fd, SOCKETADDRESS *sa, SOCKETADDRESS *netif, jint timeout, jint ttl)
看到上述代码就十分明了了,注释也十分清晰。isReachable0
这个Native方法会先尝试创建一个发送ICMP包的SOCK_RAW
类型的socket,而创建这种socket需要用户有足够的权限,比如root权限,否则创建socket就会失败(返回-1)
若用户有足够的权限,则socket创建成功,此时通过ping4()
函数来进行连通性测试,有兴趣的读者可以看这个函数的实现,其本质上就是socket编程发送ICMP包来检测,原理和ping
命令类似
而当用户权限不足创建socket失败后,源码中开始走另一条路,即调用tcp_ping4()
函数来进行连通性测试。此函数本质上是建立一个socket连接到目标主机上的echo服务 ,若echo服务有回应则认为是连通的。这里建立的socket是SOCK_STREAM
类型,即普通的面向连接的socket,普通用户就有权限创建
echo服务是一种特殊服务,工作在端口 7 上,它只是简单地将收到的东西返回给发送者,通过这个特性就能判断目的主机网络是否连通。echo协议可参考RFC 862 - Echo Protocol (ietf.org)
说到这里,我们再回头看一开始的测试代码。既然在程序权限不足的情况下底层实现会通过echo协议来判断连通性,那为什么这个测试代码还是失败了呢?有经验的同学可能已经猜到了,正是防火墙在作祟
从上图可以看出,我们使用普通用户权限也能正常进行连通性测试了,但前提是目标主机开放了echo服务所在的7端口
总结
通过上文的测试和分析,可以得出下述结论(Linux平台下):
- Java中
InetAddress.isReachable()
方法底层有两种机制来判断连通性,一种是使用ICMP报文,在程序有足够权限时使用;另一种则是使用echo协议,在程序权限不足以建立原生socket发送ICMP报文时使用。 - 当程序权限不足时会使用echo协议来进行连通性测试,此时需要目标主机端口 7 为开放状态才有意义。端口 7 未开放时也会测试失败
基于上述结论,这里给出使用InetAddress.isReachable()
时的一些注意事项:
- 判断程序上线时是否有足够的权限,若是以普通用户权限运行则不建议使用
InetAddress.isReachable()
进行连通性测试 - 若权限不足但又非要使用
InetAddress.isReachable()
来进行连通性测试,则需要考虑被测试的目标主机是否会正常开放端口 7。若能保证被测的目标主机端口 7 都是开放的,那也可以使用此方法 - 默认情况下,笔者测试的CentOS7和Ubuntu 22.04系统中端口 7 都是默认关闭状态,使用时需要谨慎
----未经授权,请勿转载----