首先,需要确认cidr是什么?cidr表示的是无类别域间路由,通常形式是ip地址后跟一个斜杠和数字,比如192.168.1.0/24。这个数字表示网络前缀的位数,剩下的位数用于主机地址。cidr的作用是更高效地分配ip地址,减少浪费。
我们现在要实现,将给定的起始ip和结束ip转换为cidr块。那么,如何从两个ip地址中找到覆盖它们的cidr呢?可能需要分解成多个cidr块,因为单个cidr可能无法覆盖整个范围,尤其是当范围不是连续的2的幂次方时。
一、需要考虑以下几点
将ip地址转换为整数:ipv4地址是32位的,可以将每个部分转换为二进制,然后组合成一个长整数,这样便于计算。例如,192.168.1.0转换为整数是3232235776。
确定起始和结束ip的整数形式:需要将输入的字符串拆分四个部分,每个部分转换为整数,并进行位移组合成一个长整型。
计算cidr块:从起始地址开始,找到最大的前缀长度(即最小的子网),使得该子网包含在起始和结束范围内。然后,将起始地址递增到下一个子网的起始地址,重复这个过程直到覆盖整个范围。
这里的关键步骤是如何找到每个可能的cidr块。例如,假设起始ip是192.168.0.0,结束ip是192.168.3.255。这个范围可以被分解为192.168.0.0/23或者192.168.0.0/24和192.168.1.0/24以及192.168.2.0/23等,但需要找到最合适的分解方式。
二、可能的算法步骤
- 将起始ip和结束ip转换为长整数。
- 如果起始ip大于结束ip,则交换它们。
- 初始化一个列表来保存cidr块。
- 循环处理,直到起始ip超过结束ip:
- 计算起始ip的最低位有效位(即最大的后缀0的位数),这决定了当前可能的最大cidr块。
- 同时,确定当前块的大小,不超过结束ip。
- 确定实际可以使用的掩码位数,使得该子网不超过结束ip。
- 将cidr块添加到列表。
- 将起始ip加上当前块的大小,继续处理。
这里需要注意的是,每次找到的cidr块应该是最大的可能块,以减少块的数量。例如,如果起始ip是192.168.0.0,结束ip是192.168.0.255,那么cidr是/24。但如果结束ip是192.168.1.127,可能需要分解为多个块,如/24和/25等。
在java中实现时,需要处理大整数,因为ipv4是32位,可以使用long类型来避免符号问题。同时,掩码的计算可能需要位运算,比如找出最长的公共前缀。
另外,还需要处理一些特殊情况,比如起始和结束ip相同,这时候cidr就是/32。或者起始ip到结束ip是一个连续的块,但无法被一个cidr覆盖,需要拆分成多个。
例如,起始ip是192.168.0.5,结束ip是192.168.0.7。这时候cidr列表应该是192.168.0.5/32, 192.168.0.6/31,或者类似的组合。但需要确保每个cidr块覆盖的ip范围在起始和结束之间。
三、java程序实现步骤
现在,需要将这些逻辑转化为java代码,需要以下步骤:
- 编写一个方法将ip字符串转换为长整数。
- 编写一个方法将长整数转换回ip字符串。
- 编写方法计算两个ip之间的cidr块列表。
在转换ip到长整数时,需要将每个部分左移相应的位数,例如第一个部分左移24位,第二个左移16位,依此类推。同时,确保每个部分的数值在0-255之间。
在计算cidr块时,需要找到最大的掩码位数,使得该子网的起始地址加上块大小不超过结束ip。块大小是2^(32 - mask),所以需要找到最大的mask,使得start + size <= end + 1。或者,另一种方式是找到起始ip的最低位有效位,然后逐步调整掩码位数。
3.1 如何计算mask?
可以计算从start到end的差值为diff = end - start +1。然后,最大的块是最大的2的幂次方,不大于diff,并且块的大小还要考虑start的低位是否为0。
或者,找到从start开始的最低位有效位,例如,计算start的二进制中最低位的1的位置,然后块的大小是2^该位置,但是不能超过end - start +1。
例如,start的二进制是x,那么最大的块大小是最大的2的幂次方,使得块大小 <= diff,并且块大小是start的二进制中最低位1所代表的值。
例如,假设start是192.168.0.5(转换为整数是3232235525),二进制是…00000101。最低位的1的位置是0(因为最后一位是1),所以块大小是1,即mask是32。这样该块只包含start自己。但如果diff大于1,那么可能之后的下一个块可以更大。
或者,这可能不是最优的方式。正确的做法应该是找到最大的mask,使得该子网覆盖的起始地址是start,并且子网的结束地址不超过end。
这可能需要计算该子网的网络地址,然后找到最大的mask,使得该子网的广播地址不超过end。
例如,子网的网络地址是start & mask,而广播地址是network | (~mask & 0xffffffffl)。所以需要确保广播地址 <= end。
但这个过程可能比较复杂。因此,可能需要另一种方法:
对于当前的start,尝试找到最大的mask,使得该mask对应的块大小(即2^(32 - mask))使得start + block_size -1 <= end.
同时,mask必须满足start的二进制中,后(32 - mask)位必须全为0,否则该子网的起始地址不是start,而是更小的地址。
例如,如果start是192.168.0.5,而mask是24,那么子网的起始地址是192.168.0.0,而start不在该子网的起始地址,因此无法使用mask为24。因此,必须找到最大的mask,使得start是该子网的起始地址,即start的后(32 - mask)位必须是0。
因此,正确的步骤应该是:
计算从start开始,最大的后缀0的位数。这可以通过计算start的最低位1的位置,比如start的二进制中有n个后缀0,那么最大的mask是32 - n。此时,块大小是2^n。例如,start是192.168.0.0(即…00000000),那么n是8,mask是24,块大小是256。
但如果start是192.168.0.128,二进制是…10000000,那么后缀0的位数是7,所以mask是25,块大小是128。此时,子网是192.168.0.128/25,广播地址是192.168.0.255。
但是,如果end是192.168.0.255,那么这个块可以包含。但是如果end是更小的值,比如192.168.0.200,那么需要调整mask,使得块大小不超过end - start +1。
因此,mask的确定应该是:
mask = 32 - math.min(lowbit, log2(remainingsize))
其中,lowbit是start的最低位1的位置,remainingsize是end - start +1。
或者,另一种方法是:
mask = 32 - math.min(lowbit, 32 - (int)(math.log(remainingsize)/math.log(2)))
但是,计算lowbit的方式需要正确。例如,对于start的二进制,找到最右边1的位置。比如,start的二进制是…0001000,那么lowbit是3(从0开始计数),即2^3=8。所以块的大小是8,mask是32 -3 =29。
3.2 如何计算lowbit呢?
可以用位运算来找最右边的1的位置。例如,对于long类型的start,取值为start & -start,这会得到一个只有最右边1的数,然后计算log2。
例如,start & -start可以得到一个数,其中只有最右边的1被保留。例如,start是5(二进制101),start & -start的结果是1(二进制1),所以log2(1)=0,即lowbit=0。此时,块的大小是1,mask=32-0=32.
如果是start是8(二进制1000),则start & -start=8,log2(8)=3,所以lowbit=3,块的大小是8,mask=29.
所以,lowbit = log2(start & -start). 但如何计算log2?可以用位运算,或者循环右移。
在java中,可以使用long.numberoftrailingzeros()方法,这个方法返回的是二进制中最低位1后面的0的个数。例如,对于8(1000),numberoftrailingzeros是3。此时,lowbit就是这个值,即块的大小是1 << lowbit。
但这个方法是否适用于所有情况?
假设start是0,那么start & -start是0,这可能有问题。但start在这里是介于起始和结束之间的,所以可能不会为0?
但起始ip可能为0,例如0.0.0.0到0.0.0.255。此时,lowbit是8,块的大小是256,mask是24.
所以,lowbit的计算应该是:
long diff = start & -start;
如果diff ==0,那么lowbit=32?或者如何处理?
或者,另一种方式,使用long.numberoftrailingzeros(startiplong)来获取最低位1后面的0的个数。例如,对于startiplong=8(二进制1000),这个方法会返回3,因为末尾有3个0。此时,块的大小是1 << 3 =8.
所以,lowbit = long.numberoftrailingzeros(startiplong | 0xffffffff00000000l)? 不太确定,可能需要仔细测试。
或者,对于ipv4的32位,startiplong是32位的数值,存储在long类型的高32位或者低32位?可能要看转换方式是否正确。例如,前面的转换函数是否正确地将ip转换为长整数,比如将第一个八位移到高位。
例如,ip地址a.b.c.d转换为长整数的方式是:a<<24 | b<<16 | c<<8 | d。这样,ipv4的32位被放在long的高32位还是低32位?
假设转换正确的话,那么对于192.168.0.5,转换为长整数是 (192 <<24) | (168<<16) | (0<<8) |5 = 3232235525。二进制表示为:
11000000 10101000 00000000 00000101
这里,最低位的1是在第0位(最后一个字节的5是00000101,最低位是1)。所以,long.numberoftrailingzeros(3232235525)应该是0,因为最低位是1,后面没有0。所以块的大小是1,mask是32-0=32。
这样,每个块的大小是根据start的最低位1的位置决定的,但也要确保块的大小不超过剩余的地址空间(即end - start +1)。
3.3 综合起来,步骤如下
将startip和endip转换为长整数s和e,确保s <=e。
初始化结果列表。
当s <= e时,循环:
- a. 计算当前s的最低位1的位置,得到lowbit = long.numberoftrailingzeros(s).
- b. 可能的块大小maxsize = 1 << lowbit.
- c. 但maxsize不能超过e - s +1,所以实际块大小是math.min(maxsize, e -s +1).
- d. 这时候,需要找到最大的mask,使得块大小为actualsize。因为actualsize必须是2的幂,所以mask = 32 - log2(actualsize).
- e. 所以,mask = 32 - (int)(math.log(actualsize)/math.log(2)).
- f. 将cidr块添加到结果列表:ip/mask.
- g. s += actualsize.
这样,每次循环处理一个块,直到覆盖整个范围。
例如,start=192.168.0.5,end=192.168.0.7:
s=3232235525 (0xc0a80005), e=3232235527.
第一次循环:
- lowbit = trailing zeros of s=5(二进制101),即0位,所以maxsize=1<<0=1.
- actualsize=min(1, 7-5+1=3) →1.
- mask=32 -0=32.
- cidr块是192.168.0.5/32.
- s +=1 →6.
第二次循环:
- s=6,二进制00000110,trailing zeros是1(因为最后两位是 10 →最低位是0?)
- 不,long.numberoftrailingzeros(6)是1,因为6的二进制是110,末尾有一个0。
- 所以,lowbit=1 →maxsize=2.
- actualsize=min(2, 7-6+1=2) →2.
- mask=32 -1=31.
- cidr块是192.168.0.6/31,覆盖6和7.
- s +=2 →8,超出e=7,循环结束。
所以,结果是两个cidr块:/32和/31.
这似乎正确。
另一个例子,start=192.168.0.0,end=192.168.3.255:
s=3232235520, e=3232238079.
第一次循环:
lowbit=long.numberoftrailingzeros(s) →s=0xc0a80000 →二进制是11000000 10101000 00000000 00000000 →末尾有16个零?或者可能要看具体的转换方式。
如果ip地址转换为长整数的方式是每个八位作为高位,例如:
对于192.168.0.0,转换为:
192 <<24 | 168 <<16 | 0 <<8 |0 →即3232235520,二进制是11000000 10101000 00000000 00000000。
long.numberoftrailingzeros(3232235520)→计算末尾的0的个数。3232235520的二进制是:
11000000 10101000 00000000 00000000 →最后的16位都是0,所以 trailing zeros是16.
所以,lowbit=16 →maxsize=1<<16=65536.
但实际剩余的大小是 e -s +1 = 3232238079 -3232235520 +1=2560.
所以actualsize=min(65536, 2560)=2560.
但2560不是2的幂次方。这时候需要调整,找到最大的2的幂次方,不超过2560.
所以,这里可能我的之前的思路有问题,即块的大小必须是2的幂次方,而实际剩余的大小可能不是,因此需要取最大可能的块,其大小是2的幂次方,并且不超过剩余的大小。
这时候,应该怎么做?
例如,在步骤c中,maxsize由lowbit决定,即可能的块大小是2^lowbit,但可能这个大小超过了剩余的大小,因此需要将actualsize设置为最大的2的幂次方,不超过剩余的大小。
或者,另一种方式,actualsize是最大的2的幂次方,使得actualsize <= remainingsize,并且actualsize <= maxsize(即由lowbit决定的块大小)。
例如,remainingsize是2560,那么最大的2的幂次方是2048(2^11=2048),而maxsize由lowbit=16是65536,所以actualsize=2048.
此时,mask=32 -11=21.
这样,cidr块是192.168.0.0/21?但是这样的话,该块的范围是192.168.0.0到192.168.7.255,但用户的结束ip是192.168.3.255,因此这样会超出范围。这说明我的逻辑有问题。
哦,这说明原来的算法可能存在问题。可能需要重新考虑如何确定块的大小。
正确的逻辑应该是,找到最大的mask,使得该子网的起始地址是s,并且该子网的结束地址 <=e.
也就是说,子网的掩码决定了块的大小是2^(32 - mask),所以我们需要找到最大的mask(即最小的块大小),使得 s + (2^(32 - mask)) -1 <=e.
同时,该mask必须满足s的后(32 - mask)位是0,也就是s是该子网的起始地址。
例如,对于s=192.168.0.0,e=192.168.3.255.
最大的mask是22,因为块大小是2(32-22)=210=1024,而s+1024-1=192.168.3.255。这正是需要的。
所以,正确的cidr是192.168.0.0/22.
因此,在这种情况下,算法应该返回一个cidr块,而不是多个。那问题出在哪里?
块的大小应该同时满足:
该块的大小是2的幂次方,并且该块的起始地址是s。
该块的结束地址不超过e.
所以,块的大小应该是最大的可能的2的幂次方,满足 s + size -1 <=e,并且 size是最大的可能值,并且s mod size ==0.
3.4 如何计算这样的size?
算法步骤:
计算remaining = e - s +1.
初始size为最大的可能,即最大的2的幂次方 <= remaining.
同时,size必须满足 s & (size -1) ==0 (即s是该size的倍数)。
如果这两个条件满足,则size可以是该值,否则,需要将size减半,直到满足条件。
例如,对于s=192.168.0.0,e=192.168.3.255,remaining=1024.
最大的size是1024,且s mod 1024 ==0,所以可以使用size=1024,mask=22.
所以,正确的cidr是/22.
那如何将此逻辑转化为代码?
实现步骤:
计算remaining = e -s +1.
计算maxmask = 32 - (int)(math.log(remaining)/math.log(2))→但这可能不正确,因为remaining可能不是2的幂次方。
或者,找到最大的mask,使得 2^(32 - mask) <= remaining,并且 s & (2^(32 -mask) -1) ==0.
正确的算法应该是:
循环处理,每次找到最大的size(2的幂次方),使得:
size <= remaining
并且 s + size -1 <=e
并且 s & (size -1) ==0 (即s是size的整数倍)
然后,mask=32 - log2(size).
然后,将cidr块加入列表,s += size,remaining = e -s +1.
重复直到remaining=0.
现在,如何在代码中实现这一点?
步骤:
while s <=e: remaining = e -s +1; maxsize = 1 << 30; // 初始为较大的数,比如2^30 size = 0; for (int shift = 0; shift <=32; shift++) { currentsize = 1 << shift; if (currentsize > remaining) break; if ((s & (currentsize -1)) ==0) { size = currentsize; } } // 此时size是最大的满足条件的size mask = 32 - log2(size) →即mask=32 - shift. add cidr: iptostring(s) + "/" + mask s += size
但这样可能需要一个循环来找到最大的size。
或者,可以使用位运算来找到最大的可能size:
size = remaining;
然后,找到最大的size,是2的幂次方,且满足 size <= remaining,并且 (s & (size -1)) ==0.
例如:
size = integer.highestonebit(remaining); // 最大的2的幂次方 <= remaining while (size >0) { if ((s & (size -1)) ==0) { break; } size >>=1; }
这样,size被调整为最大的满足条件的size.
例如,计算size的方法:
long size = long.highestonebit(remaining);
然后,循环检查 (s & (size -1)) ==0,否则将size减半,直到找到满足条件的size.
代码可能如下:
long remaining = e - s +1; long size = long.highestonebit(remaining); while (size >0) { if ((s & (size -1)) ==0) { break; } size >>=1; } if (size ==0) { size =1; }
然后,mask=32 - log2(size). 但如何计算log2(size)?
可以用long.numberofleadingzeros来得到。例如,对于size=1024,二进制是10000000000,共11位,所以numberofleadingzeros是64-11=53。但long.numberofleadingzeros(1024)返回的是64 - 11 =53?这可能需要调整。
或者,计算log2(size)可以使用位运算:
int logsize = 63 - long.numberofleadingzeros(size);
例如,size=1024 →63 - 53(numberofleadingzeros(1024))是10 →log2(1024)=10.
所以,mask=32 - logsize.
这样,例如size=1024 →mask=22.
所以,综合以上分析,java代码的大致结构可以是:
public static list<string> iprangetocidrs(string startip, string endip) { // 转换startip和endip为long s和e list<string> cidrs = new arraylist<>(); long s = iptolong(startip); long e = iptolong(endip); while (s <= e) { // 计算remaining long remaining = e - s +1; // 计算最大的size long size = 1; int shift =0; // 找到最大的size=2^shift <= remaining,并且s & (size-1) ==0 for (shift =0; (1l << (shift+1)) <= remaining; shift++) { if ((s & ( (1l << (shift+1)) -1 )) !=0) { break; } } size = 1l << shift; // 计算mask int mask = 32 - shift; // 添加cidr cidrs.add(longtoip(s) + "/" + mask); // 更新s s += size; } return cidrs; }
四、iptolong函数
public static long iptolong(string ip) { string[] octets = ip.split("\\."); if (octets.length !=4) { throw new illegalargumentexception("invalid ip address"); } long result =0; for (int i=0; i<4; i++) { int octet = integer.parseint(octets[i]); if (octet <0 || octet >255) { throw new illegalargumentexception("invalid ip octet"); } result <<=8; result |= octet; } return result; }
这样,对于192.168.0.5
,结果是 (192 <<24) | (168 <<16) | (0 <<8) |5 = 3232235525l
.
五、longtoip函数
public static string longtoip(long ip) { return string.format("%d.%d.%d.%d", (ip >>24) & 0xff, (ip >>16) & 0xff, (ip >>8) & 0xff, ip & 0xff); }
这样,3232235525l
会被正确转换为192.168.0.5
.
六、编写主要的循环部分
public static list<string> iprangetocidr(string startip, string endip) { long start = iptolong(startip); long end = iptolong(endip); list<string> result = new arraylist<>(); if (start > end) { throw new illegalargumentexception("start ip must be <= end ip"); } while (start <= end) { // 计算当前start的最低位1的位置,确定最大的可能的mask long maxsize = 1; int shift =0; // 找到最大的shift,使得 (1 << shift) <= remaining,并且 (start & ((1 << shift) -1)) ==0 long remaining = end - start +1; // 初始shift为0,逐步增加,直到无法满足条件 for (shift =0; (1l << (shift+1)) <= remaining; shift++) { // 检查如果增加shift后,是否start的后面的位都是0 long mask = (1l << (shift +1)) -1; if ((start & mask) !=0) { break; } } long currentsize = 1l << shift; int mask = 32 - shift; result.add(longtoip(start) + "/" + mask); start += currentsize; } return result; }
综上所述,最终的java函数应该包括:
iptolong和longtoip的转换函数。
主函数处理循环,找到每个可能的cidr块。
正确计算size和mask.
七、完整代码实现
以下是完整的java 代码,并处理可能的异常和边界情况,可以将起始 ip 和结束 ip 转换为最精确的 cidr 块列表。该方法通过位运算动态计算每个 cidr 块的范围,确保覆盖所有 ip 且不重叠
import java.util.arraylist; import java.util.list; public class cidrcalculator { public static list<string> convertiprangetocidr(string startip, string endip) { long start = iptolong(startip); long end = iptolong(endip); list<string> cidrlist = new arraylist<>(); if (start > end) { throw new illegalargumentexception("起始ip不能大于结束ip"); } while (start <= end) { // 计算当前起始ip的最低位有效位(决定cidr块的大小) int suffixzeros = long.numberoftrailingzeros(start); long maxsize = 1l << suffixzeros; // 可能的块大小(2^suffixzeros) long remaining = end - start + 1; // 剩余需要覆盖的ip数量 // 实际块大小不能超过剩余范围 long actualsize = math.min(maxsize, remaining); // 计算cidr的掩码位数(32 - log2(actualsize)) int mask = 32 - (int) (math.log(actualsize) / math.log(2)); // 将cidr块加入列表 cidrlist.add(longtoip(start) + "/" + mask); // 移动到下一个块的起始ip start += actualsize; } return cidrlist; } // 将ip字符串转换为32位整数(用long存储) private static long iptolong(string ip) { string[] octets = ip.split("\\."); if (octets.length != 4) { throw new illegalargumentexception("无效的ip地址格式"); } long result = 0; for (int i = 0; i < 4; i++) { int octet = integer.parseint(octets[i]); if (octet < 0 || octet > 255) { throw new illegalargumentexception("ip八位组超出范围"); } result = (result << 8) | octet; } return result; } // 将32位整数转换为ip字符串 private static string longtoip(long ip) { return ((ip >> 24) & 0xff) + "." + ((ip >> 16) & 0xff) + "." + ((ip >> 8) & 0xff) + "." + (ip & 0xff); } // 测试用例 public static void main(string[] args) { // 示例1:单个ip(/32) test("192.168.0.5", "192.168.0.5"); // ["192.168.0.5/32"] // 示例2:连续两个ip(/31) test("192.168.0.6", "192.168.0.7"); // ["192.168.0.6/31"] // 示例3:一个完整的c类子网(/24) test("192.168.0.0", "192.168.0.255"); // ["192.168.0.0/24"] // 示例4:跨子网的ip范围 test("192.168.0.5", "192.168.0.7"); // ["192.168.0.5/32", "192.168.0.6/31"] } private static void test(string startip, string endip) { list<string> cidrs = convertiprangetocidr(startip, endip); system.out.println("ip范围: " + startip + " - " + endip); system.out.println("cidr块: " + string.join(", ", cidrs) + "\n"); } }
代码说明
核心逻辑
- 位运算计算:通过 long.numberoftrailingzeros 找到起始 ip 的最低位有效位,确定最大可能的 cidr 块大小。
- 动态调整块大小:根据剩余 ip 范围 (remaining) 和起始 ip 的对齐要求,动态调整实际 cidr 块大小。
关键方法
- iptolong():将 ip 字符串转换为 32 位整数(用 long 存储,避免符号问题)。
- longtoip():将整数转换回点分十进制格式。
- convertiprangetocidr():主方法,遍历 ip 范围生成 cidr 列表。
测试案例
覆盖单 ip、连续 ip、完整子网和跨子网范围,验证不同场景的准确性。
执行示例
输入:
convertiprangetocidr("192.168.0.6", "192.168.0.7");
输出:
cidr块: 192.168.0.6/31
输入:
convertiprangetocidr("192.168.0.5", "192.168.0.7");
输出:
cidr块: 192.168.0.5/32, 192.168.0.6/31
到此这篇关于java中ip段转cidr的原理与实现详解的文章就介绍到这了,更多相关java ip段转cidr内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论