一.实现代码
/**
* 验证IP地址范围是否有效
*
* @param ipRange 例IP范围:192.168.1.1-192.168.2.255
* @return
*/
public static boolean isValidIpRange(String ipRange) {
String[] parts = ipRange.split("-");
if (parts.length != 2) {
return false;
}
String startIp = parts[0];
String endIp = parts[1];
// 验证起始和结束IP地址是否都有效
if (!isValidIp(startIp) || !isValidIp(endIp)) {
return false;
}
// 转换起始和结束IP地址为整数并比较
int startIpInt = ipToInt(startIp);
int endIpInt = ipToInt(endIp);
// 结束IP地址必须大于或等于起始IP地址
return endIpInt >= startIpInt;
}
/**
* 根据IP范围获取范围内所有IP地址
*
* @param ipRange 例IP范围:192.168.1.1-192.168.2.255
* @return
*/
public static List<String> generateRangeIpList(String ipRange) {
String[] parts = ipRange.split("-");
String startIp = parts[0];
String endIp = parts[1];
int startIpAsInt = ipToInt(startIp);
int endIpAsInt = ipToInt(endIp);
List<String> ipList = new ArrayList<>();
for (int i = startIpAsInt; i <= endIpAsInt; i++) {
ipList.add(intToIp(i));
}
return ipList;
}
/**
* 将IP地址转换为整数(网络字节序)
*
* @param ip 192.168.1.1
* @return
*/
private static int ipToInt(String ip) {
String[] parts = ip.split("\\.");
int result = 0;
for (int i = 0; i < parts.length; i++) {
result |= (Integer.parseInt(parts[i]) & 0xFF) << (24 - (i * 8));
}
return result;
}
/**
* 将整数转换为IP地址
*
* @param ip 192.168.1.1
* @return
*/
private static String intToIp(int ip) {
return (ip >>> 24) + "." +
((ip >> 16) & 0xFF) + "." +
((ip >> 8) & 0xFF) + "." +
(ip & 0xFF);
}
/**
* 使用正则表达式进行匹配 验证IPv4地址是否有效
*
* @param ip 例IP地址:192.168.1.1
* @return
*/
public static boolean isValidIp(String ip) {
String pattern = "^((\\d{1,2}|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(\\d{1,2}|1\\d{2}|2[0-4]\\d|25[0-5])$";
return ip.matches(pattern);
}
二.代码解析
2.1. ipToInt
方法
这个方法将IP地址字符串转换为整数。
-
字符串分割:
String[] parts = ip.split("\\.");
使用点号(".")作为分隔符将IP地址字符串分割成四个部分(例如,"192"、"168"、"1"、"1")。 -
初始化结果:
int result = 0;
定义一个整数变量result
,它将存储IP地址的整数表示。 -
循环处理每个部分:对于
parts
数组中的每个部分(即IP地址的每个数字),执行以下操作:- 解析整数:
Integer.parseInt(parts[i])
将字符串转换为整数。 - 掩码处理:
& 0xFF
是一个位掩码操作,确保整数在0到255之间(因为IP地址的每个部分都应该在0到255之间)。 - 位移操作:
<< (24 - (i * 8))
将解析并掩码处理后的整数左移适当的位数。这是因为IPv4地址是一个32位的数字,从左到右,每个部分(或称为八位字节)分别占据8位。所以,第一个部分(最左边的部分)不需要移位,第二个部分需要左移8位,依此类推。
- 解析整数:
-
返回结果:
return result;
返回存储了IP地址整数表示的result
变量。
2.2. intToIp
方法
这个方法将整数转换回IP地址字符串。
- 无符号右移:
ip >>> 24
将整数右移24位,获取IP地址的第一个部分(最左边的部分)。注意,这里使用了无符号右移(>>>
),这意味着在右侧填充0而不是符号位。 - 位掩码处理:对于IP地址的每个后续部分,使用位掩码
& 0xFF
来确保只获取8位。 - 位移和拼接:
((ip >> 16) & 0xFF)
、((ip >> 8) & 0xFF)
和(ip & 0xFF)
分别获取IP地址的第二个、第三个和第四个部分。然后,使用点号(".")将这些部分拼接成一个字符串。 - 返回结果:
return
返回拼接后的IP地址字符串。
这两个方法共同实现了IPv4地址和32位无符号整数之间的转换,这在网络编程和IP地址处理中是非常常见的操作。
2.3. 位移操作解析
IPv4地址是由四个部分组成,每个部分是一个0到255之间的整数,这四个部分之间用点号(.
)分隔。例如,IP地址 "192.168.1.1" 可以分解为四个部分:192, 168, 1, 1。
在二进制表示中,每个部分(或称为八位字节)占据8位。一个IPv4地址总共是32位。如果我们想将这四个部分组合成一个32位的整数,我们需要按照它们在IP地址中的顺序,将它们放在整数的不同位置。
现在,让我们用二进制和位移操作来演示这个过程。
假设我们有以下IP地址的四个部分(转换为二进制只是为了演示):
- 192:
11000000
- 168:
10101000
- 1:
00000001
- 1:
00000001
一个32位的整数有足够的空间来存储这四个部分,每个部分占据8位。为了将它们组合在一起,我们需要将它们分别放在整数的不同位置:
- 第一个部分(192)放在最高位(最左边),不需要移位。
- 第二个部分(168)需要左移8位,因为第一个部分已经占据了最低的8位。
- 第三个部分(1)需要左移16位,因为前两个部分已经占据了最低的16位。
- 第四个部分(1)需要左移24位,因为前三个部分已经占据了最低的24位。
用位移操作<<
来表示这个过程:
- 第一个部分:
11000000
(不变) - 第二个部分:
10101000 << 8
(变为1010100000000000
) - 第三个部分:
00000001 << 16
(变为000000010000000000000000
) - 第四个部分:
00000001 << 24
(变为00000001000000000000000000000000
)
现在,我们将这四个部分组合起来,得到最终的32位整数表示:
11000000 10101000 00000001 00000001 |
在ipToInt
方法中,循环中的位移操作<< (24 - (i * 8))
确保了每个部分都被放在正确的位置上。当i
从0开始递增时,位移量从24逐渐减少到0,这样每个新的部分就会被放在之前部分的右边(即更低的位)。
这种方法允许我们轻松地将一个IP地址的四个部分转换为一个32位的整数,这个整数在网络编程中经常用于表示IP地址,因为它比字符串更加紧凑和高效。
2.4. 位移操作详细解析
首先,我们有一个初始结果result
,它初始化为0(在二进制中,所有位都是0)。
然后,我们开始循环遍历IP地址的每个部分,并对每个部分执行以下操作:
- 将当前部分转换为整数(如果它已经是字符串表示的整数,则跳过这一步)。
- 使用位与操作
& 0xFF
来确保这个整数只有8位(即,确保它的值在0到255之间)。 - 将这个8位的整数左移适当的位数,以便它出现在最终32位整数中的正确位置。
- 使用位或操作
|
将这个左移后的值添加到result
中。
这里的关键是位或操作|
。这个操作会将两个操作数的对应位进行或运算。对于任何给定的位,只要两个操作数中至少有一个是1,结果就是那个位就是1。由于我们开始时将result
初始化为0(所有位都是0),所以位或操作实际上只是将左移后的值“放置”在result
的相应位置。
现在,让我们用实际的值来演示这个过程:
假设IP地址是"192.168.1.1",我们将其分解为四个部分:192, 168, 1, 1。
- 初始化
result
为0(在二进制中,32个0)。 - 处理第一个部分(192):
- 转换为二进制:
11000000
- 左移0位(因为这是第一个部分,所以不需要移位):
11000000
- 使用位或操作添加到
result
:result = 0 | 11000000
,所以result
现在是11000000
。
- 转换为二进制:
- 处理第二个部分(168):
- 转换为二进制:
10101000
- 左移8位:
1010100000000000
- 使用位或操作添加到
result
:result = 11000000 | 1010100000000000
,所以result
现在是110000001010100000000000
。
- 转换为二进制:
- 处理第三个部分(1):
- 转换为二进制:
00000001
- 左移16位:
000000010000000000000000
- 使用位或操作添加到
result
:result = 110000001010100000000000 | 000000010000000000000000
,所以result
现在是11000000101010000000000100000000
。
- 转换为二进制:
- 处理第四个部分(1):
- 转换为二进制:
00000001
- 左移24位:
00000001000000000000000000000000
- 使用位或操作添加到
result
:result = 11000000101010000000000100000000 | 00000001000000000000000000000000
,所以result
现在是11000000101010000000000100000001
。
- 转换为二进制:
这就是最终的32位整数表示,它对应于IP地址"192.168.1.1"。注意,这个过程不涉及右移,因为我们是在构建一个更大的数,而不是从一个大数中提取一个较小的部分。
2.5.问题分析
2.5.1 问:为什么通过运行代码IP地址"192.168.1.1返回的result是-1062731519呢?而不是11000000101010000000000100000001
呢?
答:
运行ipToInt
方法并传入IP地址"192.168.1.1"时,您实际上是在处理一个32位的无符号整数。然而,在Java中,int
类型是一个有符号的32位整数,其值的范围是-2,147,483,648到2,147,483,647。
当您尝试将一个大于Integer.MAX_VALUE
(即2,147,483,647)的无符号整数存储在一个int
变量中时,会发生所谓的“整数溢出”。在这种情况下,Java会执行模2^32运算(因为int
是32位的),这意味着它会取该数值除以2^32的余数。
对于IP地址"192.168.1.1",其对应的无符号32位整数是3232235777(即192 * 2^24 + 168 * 2^16 + 1 * 2^8 + 1)。但是,当您尝试将这个值存储在一个int
变量中时,由于大于Integer.MAX_VALUE
,所以会发生溢出。
具体来说,3232235777模2^32的结果是-1062731519。这是因为3232235777和-1062731519在二进制表示上只是符号位不同(一个的最高位是0,表示正数;另一个的最高位是1,表示负数,但在无符号整数上下文中,它只是一个更大的数)。
要正确处理这种情况,您可以将结果视为一个无符号整数,或者使用其他数据类型(如long
或BigInteger
)来存储结果,以避免溢出。但是,请注意,即使使用long
,您仍然需要小心处理无符号整数的概念,因为Java的基本数据类型都是有符号的。
在大多数情况下,当您在Java中处理IP地址的整数表示时,您只需要知道如何处理这种溢出情况,并理解它在二进制层面上的工作原理。在实际应用中,您通常不需要将IP地址的整数表示转换回其十进制表示(除非是为了显示或调试目的),因此这种溢出通常不会影响代码的正确性。