存储IP范围的数据结构,允许快速搜索给定的IP(Java)

时间:2014-08-16 12:02:52

标签: search data-structures ip range cidr

我正在尝试考虑一种有效的数据结构来表示IP范围,如下所述。我知道我想要尝试的是相当容易的,我似乎无法将手指放在上面。

所以,假设我有1.1个单独的IP范围,格式为1.1.1.0 - 1.1.2.255,或者其他(但不是像1.1.1.0/24那样的CIDR格式)。

不同的范围不是连续的,因此在一端的结束和下一次的开始之间可能存在数百万的IP。如果愿意,它们可以/将以整数格式表示(即,在该示例中为16843008-16443519)。

没有已知的IP地址重叠到其他范围。

基本上,如果您感到好奇,这些范围代表ASN网络块。我需要制作一个工具来确定任何给定的IP是否适合这些范围之一 - 但工具必须相当快(理想情况下小于0.5秒)。

现在,如果我有数百或数千个这些范围,覆盖数百万个IP,并且想要查找给定的IP是否属于其中一个范围(或不是),那么最快的方法是什么,同时也不是过于记忆密集?

我可以想到几个选项:

  • 构建一个HashSet,其中包含来自所有范围的每个IP,并且只针对它进行包含(ip)。我希望那里有大约5000万个IP。记忆力很快,但似乎有点浪费?

  • 有一个TreeMap,其键是每个范围的起始IP,其值是结束IP。如果测试IP大于该密钥但小于下一个密钥,则遍历树并检查每个密钥。如果是,则调查值(即范围的结束IP),如果IP小于映射的值,则IP在范围内 - 如果不是,则没有任何意义继续并且可以假设IP不是在任何范围内。可能二元搜索树的键来更快地得出结论,而不是检查顺序?

  • 另一个想法是拥有一个HashMap,其密钥将是所有范围内的所有可能的子网(我意识到会有很多),例如“123.123.123,123.123.124,123.123.125,211.211 .211,211.211.215“等等......如果我被要求检查IP 123.123.124.144,我可以先看看它的子网(123.123.124)是否是地图中的一个键。映射的值可能是一个自定义对象,其中包含与该特定子网关联的范围的起始IP和结束IP。然后你可以用它来检查完整的IP是否适合该范围。这个特殊对象将与地图中的许多条目共享,因为显然在给定范围内可能有许多子网。

那么,有什么想法/想法/意见吗?我有一种感觉,我的第二个想法可能是一个很好的方法去?感谢您的信息...听到您的想法非常兴奋!

4 个答案:

答案 0 :(得分:3)

如果范围不包含子范围,您可以检查番石榴RangeSet https://code.google.com/p/guava-libraries/wiki/NewCollectionTypesExplained#RangeSet
事实上,我还没有分析RangeSet的时间和空间复杂性,但RangeSet似乎很好地满足了你的要求。

答案 1 :(得分:1)

我使用AVL树,IP范围作为节点值和合适的比较功能。 (当范围是a..b(a< = b)时,当比较两个范围r1和r2时:r1< r2如果r1.b< r2.a; r1“==”r2如果r1.a> ; = r2.a和r1.b< = r2.b; r1> r2如果r1.a> r2.b.所以“==”表示r1等于或包含在r2中。)

如果没有重叠,那就足够了。如果你有重叠(就像我一样,但我正在处理网络前缀),你最终会将AVL树嵌套在AVL树中。

当你说没有ASN网络块重叠时,我假设如果一个ASN有一个/ yy委托给它,你就会将父/ xx分成单独但连续的网络块。

由于您的网络块列表不会经常更改,因此您可能不需要AVL树。您可以对网络块进行排序并使用二进制文件命中列表。如果你需要比二叉树/斩波更快的东西你可以有一个辅助的指针集成二进制斩波,使用查找范围的起始点的ms字节来识别值得查看的第一个和最后一个范围在

答案 2 :(得分:0)

这是我使用的结构。让我们有另一个表,在这种情况下,表locations只是为了在真实情况下看到IP范围的目的和用途。

--
-- Table structure for table `locations`
--
CREATE TABLE IF NOT EXISTS `locations` (
  `location_id` int(10) unsigned NOT NULL,
  `parent_id` int(10) unsigned NOT NULL,
  `location_name` varchar(64) NOT NULL,
  PRIMARY KEY (`location_id`),
  KEY `parent_id` (`parent_id`)
);

--
-- Table structure for table `locations_to_ip_ranges`
--
CREATE TABLE IF NOT EXISTS `locations_to_ip_ranges` (
  `l_ip_r` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `location_id` int(10) unsigned NOT NULL,
  `starting_ip` varchar(45) NOT NULL,
  `ending_ip` varchar(45) NOT NULL,
  `starting_cidr` int(10) unsigned NOT NULL,
  `ending_cidr` int(10) unsigned NOT NULL,
  PRIMARY KEY (`l_ip_r`),
  KEY `location_id` (`location_id`)
);

以下是第二张表中的一些记录

l_ip_r  location_id starting_ip     ending_ip        starting_cidr  ending_cidr
-------------------------------------------------------------------------------
94005   47          217.147.0.0     217.147.15.255   3650289664     3650293759
94004   47          217.146.32.0    217.146.47.255   3650232320     3650236415
94003   47          217.145.144.0   217.145.159.255  3650195456     3650199551
94002   47          217.145.16.0    217.145.31.255   3650162688     3650166783
94001   47          217.144.176.0   217.144.191.255  3650138112     3650142207

以下是将IP地址转换为CIDR编号的有用功能。它是用PHP编写的,但我相信将它转换为Java很容易。此处使用的explode()函数根据给定的分隔符拆分字符串。

function ip_address_to_cidr($ip_address){
    $ips = explode(".", $ip_address);
    return ($ips[3] + $ips[2] * 256 + $ips[1] * 65536 + $ips[0] * 16777216);
}

因此,如果您想获得给定IP地址的国家/地区,您将拥有类似的内容

// call the ip_ip_address_to_cidr function passing the remote_address as an argument
$cidr = ip_address_to_cidr($_SERVER['REMOTE_ADDR']);

// pass the returned $cidr to the following query and get the location_id
$set = mysql_query("
    SELECT location_id 
    FROM locations_to_ip_ranges
    WHERE " . $cidr . " BETWEEN starting_cidr AND ending_cidr
");
$row = mysql_fetch_object($set);
echo $row->location_id 

由于我需要多语言支持,因此我遗漏了另一个表locations_to_languages以保持简单明了。目前这些表包含数千万个数据,我没有性能问题。

附注:我已经很长时间没有使用Java了,所以上面的代码片段是用PHP编写的,但我相信如果需要的话,理解逻辑并将其转换为Java并不难。 / p>

答案 3 :(得分:0)

我们可以使用btree,通过它可以映射主内存中的IP地址,然后将它们映射到辅助内存。 我们知道我们需要存储相当多的ip地址,因此我们不能将它们存储在主存储器中,如果我们使用btree则更好。 因为它类似于散列原理也有效的内存利用率。