判斷字元串是否為IP位址通常都是基于正規表達式實作的,無論是引入外部的依賴包亦或是自己寫正則實作,基本都是基于正規表達式實作的判斷。然而比較例外的是,jdk自身提供了
Inet4Address.getByName
方法也可以幫助我們實作ip位址的判斷。本文将詳細列舉常見的判斷字元串是否為IPV4,IPV6位址的方式,并分析其存在的局限性。
一、判斷是否為IPV4,IPV6位址的常見方式
1. 使用Apache Commons Validator做判斷
需要引入依賴包
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.6</version>
</dependency>
有了依賴包,後續調用
InetAddressValidator
的核心API就好了。
1.1判斷是否為IPV4位址
private static final InetAddressValidator VALIDATOR = InetAddressValidator.getInstance();
public static boolean isValidIPV4ByValidator(String inetAddress) {
return VALIDATOR.isValidInet4Address(inetAddress);
}
1.2判斷是否為IPV6位址
public static boolean isValidIPV6ByValidator(String inetAddress) {
return VALIDATOR.isValidInet6Address(inetAddress);
}
1.3判斷是否為IPV6或者IPV4位址
public static boolean isValidIPV6ByValidator(String inetAddress) {
return VALIDATOR.isValid(inetAddress);
}
2. 使用Guava做判斷
引入依賴包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>
調用
InetAddresses.isInetAddress
即可實作快速的判斷,但這個方式能同時判斷字元串是否為IPV4或者IPV6位址,如果你隻想判斷其中一種格式,那就不行了。
public static boolean isValidByGuava(String ip) {
return InetAddresses.isInetAddress(ip);
}
3. 使用OWASP正規表達式做判斷
OWASP提供了一系列用于校驗常見web應用名詞的正規表達式,通過OWASP_Validation_Regex_Repository你可以檢索到他們。這個判斷方式隻能判斷是否為IPV4位址。
private static final String OWASP_IPV4_REGEX =
"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
private static final Pattern OWASP_IPv4_PATTERN = Pattern.compile(OWASP_IPV4_REGEX);
public static boolean isValidIPV4ByOWASP(String ip) {
if (ip == null || ip.trim().isEmpty()) {
return false;
}
return OWASP_IPv4_PATTERN.matcher(ip).matches();
}
4. 使用自定義正規表達式做判斷
如下通過自定義的正規表達式判斷字元串是否為IPV4位址,它的正規表達式以及實作細節,其實和第一種方案中判斷IPV4是一緻的,如果你隻想判斷字元串是否為IPV4位址,又懶得引入外部包,那麼3,4這兩種方式适合你。
private static final String IPV4_REGEX =
"^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$";
private static final Pattern IPv4_PATTERN = Pattern.compile(IPV4_REGEX);
public static boolean isValidIPV4ByCustomRegex(String ip) {
if (ip == null || ip.trim().isEmpty()) {
return false;
}
if (!IPv4_PATTERN.matcher(ip).matches()) {
return false;
}
String[] parts = ip.split("\\.");
try {
for (String segment : parts) {
if (Integer.parseInt(segment) > 255 ||
(segment.length() > 1 && segment.startsWith("0"))) {
return false;
}
}
} catch (NumberFormatException e) {
return false;
}
return true;
}
5. 使用JDK内置的Inet4Address做判斷
JDK從1.4版本開始就提供了
Inet4Address
類實作對IP的各項校驗操作,結合該類的
getByName
和
getHostAddress
方法可實作IP位址判斷,但是頻繁的調用這兩個方法會産生一定的性能問題。以下是通過JDK判斷字元串是否為IPV4位址的方式:
public static boolean isValidIPV4ByJDK(String ip) {
try {
return Inet4Address.getByName(ip)
.getHostAddress().equals(ip);
} catch (UnknownHostException ex) {
return false;
}
}
二、并不适合ping指令
1. IPV4的标準格式
本文列舉的幾種判斷方式都是針對标準的IP位址而言,标準指的是IP位址由4位通過逗号分割的8bit長度的數字字元串組成,由于每位數字隻有8bit長度,是以每個數字的值應該在0~255範圍内。相關文檔可以參考RFC5321。
2. 有效性驗證
我們選舉幾組字元串,有缺少位數的,有數字以0開頭的,也有一組是符合标準格式的。然後通過之前列舉的方法判斷是否為有效的IP位址。
測試過程就不再贅述,直接将測試用例和測試結果彙總成如下的表格:
用例 | isValidIPV4ByValidator | isValidIPV6ByValidator | isValidByGuava | isValidIPV4ByOWASP | isValidIPV4ByCustomRegex | isValidIPV4ByJDK |
---|---|---|---|---|---|---|
172.8.9.28 | true | false | true | true | true | true |
192.168.0.072 | false | false | false | true | false | false |
172.08.9.28 | false | false | false | true | false | false |
172.9.28 | false | false | false | false | false | false |
192.168.072 | false | false | false | false | false | false |
192.168.1 | false | false | false | false | false | false |
2001:0db8:85a3:0000:0000:8a2e:0370:7334 | false | true | true | false | false | false |
通過這7個測試用例中,不難看出:
- 第1個IP剛好是4位,每位都在0~255之間,且沒有任何一位以0開頭。所有判斷IPV4的方法都傳回了true,符合預期。
- 第2,3個IP也都是4位位址,但是某一位出現以0開始的數字,此時采用OWASP正規表達式的方式傳回了true,其他方法都傳回了false。
- 第4,5,6個IP都是3位位址,所有方法傳回了false。
- 最後一個是合法的ipv6位址,我們通過
或者Apache Commons Validator
包提供的判斷方法能夠正常傳回true。Guava
3. 性能對比
本文在列舉的第5個判斷方法時特意提到了性能問題,那麼使用
Inet4Address
判斷IP位址到底會導緻多大的性能損耗呢?實驗證明,當判斷使用大規模非法IP位址做輸入,該方法的性能損耗将不敢想象!
下面将通過一項測試來驗證這個結論。
private static List<String> generateFakeIp(int capacity) {
List<String> ipList = new ArrayList<String>(capacity);
for (int i = 0; i < capacity; i++) {
int parts = boundRandom(1, 3);
if (chanceOf50()) { //each ip has 50% chance to be 4 parts
parts = 4;
}
StringBuilder sbBuilder = new StringBuilder();
for (int j = 0; j < parts; j++) {
if (sbBuilder.length() > 0) {
sbBuilder.append(".");
}
StringBuilder stringBuilder = new StringBuilder();
if (chanceOf10()) { //each part has 10% chance to generate a fake number
stringBuilder.append('a');
} else { //each part has 90% chance to generate the correct number
stringBuilder.append(boundRandom(0, 255));
}
sbBuilder.append(stringBuilder);
}
ipList.add(sbBuilder.toString());
}
return ipList;
}
private static long correctCount(List<String> ipList) {
return ipList.stream().filter(ip -> isValidIPV4ByCustomRegex(ip)).collect(Collectors.toList()).size();
}
// 50% chance
private static boolean chanceOf50() {
return boundRandom(0, 9) < 5;
}
// 10% chance
private static boolean chanceOf10() {
return boundRandom(0, 9) < 1;
}
private static Random random = new Random();
// random int between [start, end], both start and end are included
private static int boundRandom(int start, int end) {
return start + random.nextInt(end);
}
我們通過上面的
generateFakeIp
方法來産生一批随機的IP位址,這些IP中有正确格式的,也有非法格式的。
主體測試方法如下,該方法将比較
isValidIPV4ByCustomRegex
和
isValidIPV4ByJDK
這兩種判斷IP位址的總耗時,以分析性能問題。
public static void performanceTest() {
List<String> ipList = generateFakeIp(100);
double chance = correctCount(ipList);
System.out.println("start testing, correct ip count is : " + chance);
long t1 = System.currentTimeMillis();
ipList.stream().forEach( ip-> isValidIPV4ByCustomRegex(ip));
long t2 = System.currentTimeMillis();
ipList.stream().forEach( ip-> isValidIPV4ByJDK(ip));
long t3 = System.currentTimeMillis();
System.out.println("isValidIPV4ByCustomRegex cost time : " + (t2-t1));
System.out.println("isValidIPV4ByJDK cost time : " + (t3-t2));
}
直接運作後,列印以下結果。
start testing, correct ip count is : 37.0
isValidIPV4ByCustomRegex cost time : 2
isValidIPV4ByJDK cost time : 13745
可以看到,當100個IP中隻有37個是合法IP時,基于正規表達式的判斷方法隻用了2ms,而基于JDK内置的
Inet4Address
實作的判斷方法卻用了13s,這已經不在在同一個數量級了。如果我們将測試基數再擴大,那更加不敢想象,是以實際工作中,千萬不要使用
Inet4Address
來做IP合法性判斷。
4. 判斷IPV4的方法并不适合ping指令
對于标準IPV4格式的位址來說,以上判斷方式是沒問題的,但是部分非标準IPV4格式的位址,卻能夠被ping指令正常解析。
對于ping指令來說,我們這裡列舉的第2~6個IP位址都是合法的,能夠被正常解析。
不妨驗證一下:
可以看出,當我們輸入的IP位址中,某一位數字位以0開頭,那麼也能被正常解析,從圖檔可以看出
192.168.0.072
被解析成了
192.168.0.58
,
172.08.9.28
被解析成了
172.08.9.28
。這是為什麼呢?
當ping指令接收的IP位址中,出現以0開頭的數字位,那麼ping指令将嘗試以八進制解析該位,八進制的072,即對應十進制的58,是以
192.168.0.072
就被解析成了
192.168.0.58
。
如果以0開頭的數字位,不符合八進制的格式,則依然以十進制對待該數字位,并忽略最高位的0,由于
172.08.9.28
中
08
并不是一個合法的八進制數,是以依然按十進制對待并忽略最高位的0,即實際解析成
172.8.9.28
此外,當輸入的IP位址并不是以逗号分割的四位,ping指令依然能夠正常解析。分别ping
196.168.072
,
192.168
,
196
時,實際被解析成了
196.168.0.072
,
196.0.0.168
,
0.0.0.192
可以看出,當IP不足四位時,ping指令會在合适的位置補0,其規律如下所示:
1 part (ping A) : 0.0.0.A
2 parts (ping A.B) : A.0.0.B
3 parts (ping A.B.C) : A.B.0.C
4 parts (ping A.B.C.D) : A.B.C.D
三、小結
這幾種判斷字元串是否為IPV4或者IPV6位址的方式,其内在實作原理都大同小異,除了最後一個方案外都是用正規表達式來實作的。
但是基于正規表達式實作的方法并不能很友好地處理非十進制數字位的情況,而ping指令能夠接收的字元串遠比這個複雜的多,如果你想通過Java來實作判斷ping指令後面的位址是否是合法的IP,那應該是難于上青天,除非你去弄懂ping指令的底層源碼。
當然在現實業務場景中,我們判斷字元串是否為合法IP位址一般都是基于其标準格式來操作的,也不用擔心文中的方法不靠譜,除了第3和最後一個方案外,大膽用吧!