随着IPv6在慢慢应用普及,很多公司内部应用都需要逐渐支持IPv6。但由于之前大部分开发人员都只考虑了IPv4,导致升级支持IPv6时工作量较大,甚至会出现大量的if else的臃肿代码。
本ip解析模块jip-common,它对上层应用直接屏蔽IP类型以及IP段等底层概念,使得上层应用切换升级更加方便。
更多详情通过以下链接了解:
- gitee:https://gitee.com/toktok
- blog: http://www.easysb.cn
jip-common不仅仅支持IPv4和IPv6,还支持多种IP格式的解析(参考下一节),可以对上层应用直接屏蔽IP的类型,甚至单个IP还是IP段,使得应用上层代码更加简洁。考虑到大部分业务都需要IP的集合操作,JIP库还提供了IP集合的操作,比如合并、交集、并集和差集功能,最大限度地满足业务的需求。
通常,我们为了实现查找某个IP是属于哪个产品时,很多开发人员的做法就直接将IP段散列成单个IP,然后通过Map的方式一一对应起来,从而实现快速查找。这对于ip数量不是很多的情况下,没有太多的问题。倘若IP数量较大,或者说了到IPv6这个层面,那么这种方法简直就是要被开除的节奏了。为此,JIP模块基于线段树和红黑树的特性,不再散列IP段,实现了一种快速搜索查找的功能,可以大大节省内存和搜索时间。
- maven
- gradle等
请参考如下链接
https://jitpack.io/#jekkay/jip-common/
对于# 开头的一行,JIP会默认为注释,跳过解析。
IP之间的分隔符可以是 "\r\n", " \r", "\n", ";"四种。
-
IPV4支持的格式有四种,分别如下:
格式 | 举例说明 | 备注 |
---|---|---|
单个IP格式 | 1.1.1.1 1.1.1.1 # 还可以支持注释哦 |
注释中不要带分号,会被认为多个ip的分割符号 |
ip段带掩码 | 1.1.1.1/24 1.1.1.1/24 # 还可以支持注释哦 |
注释中不要带分号,会被认为多个ip的分割符号 |
IP段简单起始样式 | 1.1.1.1-20 1.1.1.1-20 # 还可以支持注释哦 |
注释中不要带分号,会被认为多个ip的分割符号 |
IP段完全起止样式 | 1.1.1.1-1.1.1.20 1.1.1.1-1.1.1.20 # 还可以支持注释哦 |
注释中不要带分号,会被认为多个ip的分割符号 |
-
IPV6支持的格式有四种,分别如下:
格式 | 举例说明 | 备注 |
---|---|---|
单个IP格式 | 8888::226:2dff:fefa:0 8888::226:2dff:fefa:0 # 还可以支持注释哦 |
注释中不要带分号,会被认为多个ip的分割符号 |
ip段带掩码 | 8888::226:2dff:fefa:0/24 8888::226:2dff:fefa:0/24 # 还可以支持注释哦 |
注释中不要带分号,会被认为多个ip的分割符号 |
IP段简单起始样式 | 8888::226:2dff:fefa:0-20 8888::226:2dff:fefa:0-20 # 还可以支持注释哦 |
注释中不要带分号,会被认为多个ip的分割符号 |
IP段完全起止样式 | 8888::226:2dff:fefa:0-20 8888::226:2dff:fefa:0-8888::226:2dff:fefa:20 # 还可以支持注释哦 |
注释中不要带分号,会被认为多个ip的分割符号 |
JIP对上层提供了统一个IP接口JIPAddress,并实现了一个IP集合JIPAddressSet的功能,它的底层就是基于线段树和红黑树特性实现的,完成实现集合常规的业务操作。值得说明的是,这个集合可以同时包含上述的3.1所支持的格式,也就说是上层应用已经不用去关心该IP是IPv4还是IPv6,到底是单个IP还是IP段。
一般详细的说明文档,看的人会比较少,那么我就直接按照使用场景来说明JIP模块能为咱带来什么便利,一起来看吧。
- 单个IP或单个IP段
解析单个IP或者IP段时,可以直接使用
JIPAddress a1 = JIPAddressUtils.toIpObject("10.0.0.5/24 # 这是注释");
System.out.println(a1.toString()); // 10.0.0.5/24
JIPAddress a2 = JIPAddressUtils.toIpObject("10.0.0.0-255");
JIPAddress a3 = JIPAddressUtils.toIpObject("8888::226:2dff:fefa:10-8888::226:2dff:fefa:20")
如果要想知道解析的结果是什么类型,可以调用 getIpType()函数, 它会返回一个枚举的类型,关于枚举的定义请参考源码。
System.out.println(a1.getIpType())
- 多个IP或多个IP段
假如要对多个IP或者多个IP段解析时,可以直接使用如下方法。
JIPAddressSet addressSet = JIPAddressUtils.buildAddressSet(
"101.35.129.0;101.35.132.0/24;101.35.133.0/24;101.35.134.0/24;101.35.135.0/24;");
就可以直接构造出一个IP集合,多个IP可以用四种符号隔开[\r\n, \r, \n以及分号(;)
],IPv4和IPv6可以并存,在其中也可以增加注释也不会有任何问题。
- 判断是否有效IP地址
JIPAddressUtils.isValidIPAddress("1.1.1.1")
IP地址合并,主要是为了减少IP条目数的数量,比如1.1.1.0-100,和1.1.1.100-255就可以合并成1.1.1.0/24。JIP可以提供单个IP(段)的合并,也提供了多个IP(段)的合并功能,主要由 JIPAddressCombiner
完成。
下面以单个IP(段)合并为例
JIPAddress jipAddress1 = JIPAddressUtils.toIpObject("1.1.1.0");
JIPAddress jipAddress2 = JIPAddressUtils.toIpObject("1.1.1.1-255");
List<JIPAddress> result = JIPAddressCombiner.combine(jipAddress1, jipAddress2);
// 输出就是一个1.1.1.0/24
同样的道理,多个IP(段)的合并也是类似的。
IP地址的交集功能,不仅仅是求单IP(段),也可以就多个IP(段)之间的交集,该功能主要由 JIPAddressIntersecter
完成。
以下以单个IP(段)的交集为例
JIPAddress jipAddress1 = JIPAddressUtils.toIpObject("1.1.1.15-30");
JIPAddress jipAddress2 = JIPAddressUtils.toIpObject("1.1.1.10-20");
List<JIPAddress> result = JIPAddressIntersecter.intersect(jipAddress1, jipAddress2);
// 输出就是一个 1.1.1.15-20
同样的道理,多个IP(段)的交集也是类似的。
JIPAddressSet addressSet1 = JIPAddressUtils.buildAddressSet(
"101.35.129.0;101.35.132.0/24;101.35.133.0/24;101.35.134.0/24;101.35.135.0/24;");
JIPAddressSet addressSet2 = JIPAddressUtils.buildAddressSet("101.35.129.0;101.35.132.0/24;");
List<JIPAddress> addressList = JIPAddressIntersecter.intersect(
addressSet1.all(), addressSet2.all());
IP地址的并集功能,不仅仅是求单IP(段),也可以就多个IP(段)之间的交集,该功能主要由 JIPAddressUnioner
完成。
以下以单个IP(段)的交集为例
JIPAddress address1 = JIPAddressUtils.toIpObject("101.35.135.0/24");
JIPAddress address2 = JIPAddressUtils.toIpObject("101.35.136.0/21");
List<JIPAddress> addressList = JIPAddressUnioner.union(address1, address2);
同样的道理,多个IP(段)的并集也是类似的。
IP地址的差集功能,不仅仅是求单IP(段),也可以就多个IP(段)之间的差集,该功能主要由 JIPAddressSubtracter
完成。
以下以单个IP(段)的交集为例
JIPAddress address1 = JIPAddressUtils.toIpObject("101.35.135.0/24");
JIPAddress address2 = JIPAddressUtils.toIpObject("101.35.135.0/28");
List<JIPAddress> addressList = JIPAddressSubtracter.subtract(address1, address2);
同样的道理,多个IP(段)的差集也是类似的。
- 查找
该场景应该是最常见的,就是一个群组包含了很多IP(段),如何快速地找到IP对应的群组。通常,我们的做法是把IP段散列成一个一个,然后放在HashMap中。这种方法的问题在于IP的数量必须在一定的范围之内。倘若有个A段,甚至有个0.0.0.0/0,估计程序就吃掉大量的内存,很有可能引起程序的崩溃。
推荐的做法,就是使用JIP库的集合模块,可以快速构建一个占用内存很小的红黑树集合,如下:
// 创建一个空集合
JIPAddressSet tmpJipAddressSet = JIPAddressUtils.buildEmptyAddressSet();
for (IdcIpSegment idcIpSegment : tmpIdcIpSegmentList) {
// 将该群组对应的ip列表,全部装载在集合中,并设置外带数据为群组即可
// getIpLlist() 为带分隔符号的ip列表,允许ipv4和ipv6并存,比如1.1.1.0/24\n2.2.2.-100....
// 外带数据idcIpSegement,对应的IP信息中会自动存储映射关系
JIPAddressUtils.addIpList(tmpJipAddressSet, idcIpSegment.getIpList(), idcIpSegment);
}
那么如何查找IP对应的群组,比如群组中有个ip是1.1.1.0/24,那么我们就可以以下方法查询
JIPAddress find = jipAddressSet.findIp("1.1.1.23")
那么查询的结果find就是1.1.10/24,然后我们就可以取出它对应的群组是哪个:
IdcIpSegment findGroup = ((IdcIpSegment) find.getData())
以上两步操作可以一步到位,两种方法等价。
IdcIpSegment findGroup = jipAddressSet.findIpData("1.1.1.23", IdcIpSegment.class);
是不是很简单?!
- 注意事项①: 如果有多个满足条件的话,findIp只返回满足条件的第一个。
- 注意事项②: 集合转换成List,只需要调用all()方法,会通过前序方法遍历收集。
- 更新
如果删除集合中的IP(段),只需要调用JIPAddressSet.deleteIp()
即可,值得注意的是,删除是全量匹配,和查找是模糊匹配(有交集就行),比如:
JIPAddressSet tmpJipAddressSet = JIPAddressUtils.buildEmptyAddressSet();
JIPAddressUtils.addIpList(tmpJipAddressSet,“1.1.1.0/24”, idcIpSegment);
tmpJipAddressSet.deleteIp("1.1.1.0"); // 无法删除
tmpJipAddressSet.deleteIp("1.1.1.0/24"); // 可以删除
tmpJipAddressSet.deleteIp("1.1.1.0-255"); // 可以删除
- 有效检测
再添加IP群组的时候,通常会去检测输入的IP地址是否有效,JIP库提供了一个简单的检测分析类,它碰到第一个错误就会停止解析,并且返回错误信息:
String check = JIPAddressCheckUtils.checkConvertIpObject(addressList, departmentIPSegment.getIpList());
if (StringUtils.isNotBlank(check)) { // 如果有错误就会显示出来
return check;
}
- 冲突检测
在更新IP群组的时候,需要检测更新添加会不会和现有的冲突,常用的方法是:
-
对于所有群组构建一个大的集合树T,参考4.6。
-
对更新群组中的每个IP(段),都去集合树T中查找是否已经存在于别的群组中。
格式化输出只影响ip段的格式输出,而对于单个ip则不受影响,永远只输出单个ip的样式。
- 格式化
JIPAddress address = JIPAddressUtils.toIpObject("1.2.3.0-255");
System.out.println(address.toString()); // ===> 1.2.3.0-255
System.out.println(JIPAddressUtils.toIpListString(address, IPFormatType.SEGMENT_WITH_MASK_FIRST)); // ==> 1.2.3.0/24
System.out.println(JIPAddressUtils.toIpListString(address, IPFormatType.SEGMENT_SIMPLE_FIRST)); // ===> 1.2.3.0-255
System.out.println(JIPAddressUtils.toIpListString(address, IPFormatType.SEGMENT_FULL_FIRST)); // 1.2.3.0-1.2.3.255
格式化化输出,是只优先使用哪种格式输出,倘若无法按照指定样式输出,则自动回退使用本身的格式。比如 1.2.3.1-3
,这个ip段无法使用带掩码的形式输出,所以即便指定了mask格式,也是无效的,比如:
JIPAddress address = JIPAddressUtils.toIpObject("1.2.3.1-3");
System.out.println(address.toString()); // ====> 1.2.3.1-3
System.out.println(JIPAddressUtils.toIpListString(address, IPFormatType.SEGMENT_WITH_MASK_FIRST)); // ====> 1.2.3.1-3
System.out.println(JIPAddressUtils.toIpListString(address, IPFormatType.SEGMENT_SIMPLE_FIRST)); // ====> 1.2.3.1-3
System.out.println(JIPAddressUtils.toIpListString(address, IPFormatType.SEGMENT_FULL_FIRST)); // ====> 1.2.3.1-1.2.3.3
倘若需要将IP段散列展开成一个一个的ip,那么常用的方法是 JIPAddressUtils.expandIpList,如下
List<JIPAddress> ipList = JIPAddressUtils.expandIpList(
JIPAddressUtils.toIpObject("1.1.1.1/24"), 10);
内部通过不断调用iterator枚举,最终得到的就是前10个单IP(第二个参数表示散列出来最大ip数量,0表示没有限制)。
除了针对一个IP段,也可以对单个ip和多个IP(段)进行展开散列,如下就是获取所有的散列单个ip:
List<JIPAddress> ipList = JIPAddressUtils.expandIpList(JIPAddressUtils.buildAddressSet("1.1.1.1/30\n2.2.2.2/30"), 0);
- Fork 本仓库
- 在我的博客留言: http://www.easysb.cn