【问题标题】:Data structure to store IP ranges, allowing quick search for given IP (Java)存储 IP 范围的数据结构,允许快速搜索给定的 IP (Java)
【发布时间】:2014-08-16 12:02:52
【问题描述】:

我正在尝试一种有效的数据结构来表示 IP 范围,如下所述。我知道我想尝试的东西很容易实现,但我似乎无法确定。

假设我有数百个单独的 IP 范围,格式为 1.1.1.0 - 1.1.2.255 或其他格式(但不是 CIDR 格式,如 1.1.1.0/24)。

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

不会有已知的 IP 地址与其他范围重叠。

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

现在,如果我有成百上千个这样的范围,涵盖数百万个 IP,并且想要查找给定 IP 是否在其中一个范围内(或不在),那么最快的方法是什么?内存太密集?

我能想到几个选项:

  • 构建一个包含所有范围内的每个 IP 的 HashSet,然后针对它执行 contains(ip)。我预计那里有大约 5000 万个 IP。速度快,但似乎有点浪费,内存明智?

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

  • 1234563 .211, 211.211.215", etc... 然后,如果我被要求检查 IP 123.123.124.144,我可以首先查看它的子网 (123.123.124) 是否是地图中的一个键。映射的值可能是一个自定义对象,其中包含与该特定子网关联的范围的开始和结束 IP。然后您可以使用它来检查完整的 IP 是否适合该范围。这个特殊对象将与映射中的许多条目共享,因为显然在给定范围内可能有许多子网。

那么,有什么想法/想法/意见吗?我有一种感觉,我的第二个想法可能有一条好路要走?感谢您提供信息...很高兴听到您的想法!

【问题讨论】:

  • 我越想这个问题,我就意识到这个问题对我来说很容易解决。只需保留一个庞大的 HashMap,其中的键是每个范围中每个可能的子网。它的最大尺寸为 1764705,但对我来说,它只会是其中的 5%——所以它不是太大。我忘记了子网中的每个 IP 都可以被同等对待,因为 ASN 的分解不能小于子网级别。

标签: search data-structures ip range cidr


【解决方案1】:

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

【讨论】:

    【解决方案2】:

    我使用AVL树,以IP范围作为节点值和合适的比较函数。 (其中范围是 a..b (a = r2. a and r1.b r2 if r1.a > r2.b. 所以“==”表示 r1 等于或包含在 r2 中。)

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

    当您说没有 ASN 网络块重叠时,我假设如果 ASN 有一个 /yy 委派给它,那么您将父 /xx 分解为单独但连续的网络块。

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

    【讨论】:

      【解决方案3】:

      这是我使用的结构。让我们有另一个表,在这种情况下,表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了,所以上面的sn-p是用PHP写的,但我相信理解逻辑并在需要时将其转换为Java并不难。

      【讨论】:

      • 嗨,当然它可以很容易地在数据库级别完成,但这太慢了,它遍历了太多的数据。我正在寻找一种合适的数据结构,用于内存搜索,而无需遍历所有内容。我不是在寻找数据库模式。不过感谢您的详细回答!
      【解决方案4】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-11-01
        • 2015-11-23
        • 1970-01-01
        • 2019-08-19
        • 2019-01-23
        • 2022-10-24
        • 2014-05-16
        • 1970-01-01
        相关资源
        最近更新 更多